Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/t1/wunderbar
Code-first, low ceremony Consumer Driven Contracts test tool for Java natives
https://github.com/t1/wunderbar
api-first code-first consumer-driven-contracts graphql jakarta-ee java microprofile microprofile-graphql microprofile-restclient quarkus
Last synced: about 22 hours ago
JSON representation
Code-first, low ceremony Consumer Driven Contracts test tool for Java natives
- Host: GitHub
- URL: https://github.com/t1/wunderbar
- Owner: t1
- License: apache-2.0
- Created: 2021-02-12T16:44:19.000Z (almost 4 years ago)
- Default Branch: trunk
- Last Pushed: 2024-10-23T22:24:14.000Z (26 days ago)
- Last Synced: 2024-10-24T11:29:40.899Z (26 days ago)
- Topics: api-first, code-first, consumer-driven-contracts, graphql, jakarta-ee, java, microprofile, microprofile-graphql, microprofile-restclient, quarkus
- Language: Java
- Homepage:
- Size: 1.16 MB
- Stars: 7
- Watchers: 4
- Forks: 0
- Open Issues: 11
-
Metadata Files:
- Readme: README.adoc
- License: LICENSE
Awesome Lists containing this project
README
= BAR :: Behaviour ARchive image:https://maven-badges.herokuapp.com/maven-central/com.github.t1/wunderbar.junit/badge.svg[link=https://search.maven.org/artifact/com.github.t1/wunderbar.junit] image:https://github.com/t1/wunderbar/actions/workflows/maven.yml/badge.svg[link=https://github.com/t1/wunderbar/actions/workflows/maven.yml]
:toc: macro[.right]
toc::[]Code-first, low ceremony https://martinfowler.com/articles/consumerDrivenContracts.html[Consumer Driven Contracts] test tool for Java natives.
Let's pick that apart:
"code-first": there's a lot of debate about designing APIs schema-first (a.k.a.
API-first) vs. code-first.
I absolutely agree that (not only for public APIs) it's very important to design APIs in a consistent and easily approachable way.
I even think that it's actually more important to capture the real use-cases of real consumers of an API.
It's just way too easy to design something that looks good as a schema but is clumsy to use in practice.
And by lowering the bar to specify the (changes to an) API as much as possible, consumers are invited to help evolve the API in a pragmatic way that is easy to use."low ceremony": use it just like you'd use https://site.mockito.org[Mockito] to test your client code.
Easily scale that from Unit to Integration Testfootnote:[The terms "Integration Test", "System Test", and "Acceptance Test" are used in other contexts with slightly different meaning. This is definitively confusing, but introducing new terms or even numbers would make it even harder to understand. So this is the lesser of two evils.], i.e. integrate your client code with an actual http server mocking the responses the tests expect, in order to also cover all the (de)serialization involved.
Or even go for System Tests, where your service runs in a production-like environment and calls a pre-deployed mock service.
Either way, this helps clients to test their own code and..."Consumer Driven Contracts": ... as a _side effect_, WunderBar also records the expected http interactions into a Behavior ARchive `bar`, a simple zip file containing properties files for the method, uri, and headers, and json files for the bodiesfootnote:[We currently don't see the necessity to support other content types, open an issue if you _do_ need `xml` or whatever.].
Hand this file over to the API providers, so they can use it to verify compliance with the clients' requirements.
See the article linked above.
Just to make it clear: just because the consumer drives the API contract doesn't mean that the provider is expected to comply blindly.
It's only a starting point for the "contract negotiation".
The provider has to evolve a domain model that is consistent over all consumers: the BAR files are not a replacement for talking; they just bring it to the precision that the machines foolishly insist on; and a long-term test suite of acceptance tests.
Starting from the API Consumer perspective is actually just a very natural way of defining an API: from the real requirements.
But you must generally take care that no client application logic seeps into your backend domain, i.e. views, flows, etc."Java native": The consumer/client can use REST via https://github.com/eclipse/microprofile-rest-client[MicroProfile REST Client] or https://graphql.org[GraphQL] via https://github.com/smallrye/smallrye-graphql/tree/main/client/api[SmallRye GraphQL Client], while the server can use any technology stack that can run JUnit 5 tests (or you can use the `bar` files directly).
For the details see below.== 2 Minute API Consumer Intro
Say you're developing an Order service that uses data from a Product service, i.e. it _consumes_ an API.
You probably have a `ProductsGateway` or `ProductsResolver` class that uses a `ProductsClient` interface with annotations from https://github.com/eclipse/microprofile-rest-client[MP Rest Client] or https://github.com/smallrye/smallrye-graphql/tree/main/client/api[SmallRye GraphQL Client] that you unit-test with Mockito:[source,java]
----
@RegisterRestClient @Path("/products")
public interface ProductsClient {
@GET @Path("/{id}")
Product product(@PathParam("id") String id);
}// or alternatively
@GraphQLClientApi
public interface ProductsClient {
Product product(String id);
}// test
@ExtendWith(MockitoExtension.class)
class ProductsGatewayMockitoTest {
@Mock ProductsClient products;
@InjectMocks ProductsGateway gateway;@Test void shouldGetProduct() {
given(products.product(PRODUCT_ID)).willReturn(PRODUCT);var response = gateway.product(ORDER_ITEM);
then(response).usingRecursiveComparison().isEqualTo(PRODUCT);
}
}
----Instead of using the Mockito extension with its annotations and the static `given` method import, you can simply use those from WunderBar.
They are more limited than Mockito, but have the same style, same behavior, just different logs:[source,java]
----
@WunderBarApiConsumer
class ProductsGatewayTest {
@Service ProductsClient products;
@SystemUnderTest ProductsGateway gateway;@Test void shouldGetProduct() {
given(products.product(PRODUCT_ID)).returns(PRODUCT);var response = gateway.product(ITEM);
then(response).usingRecursiveComparison().isEqualTo(PRODUCT);
}
}
----Note that you can also use the Mockito syntax `given(...).willReturn(...)`, but using `returns` makes sure you don't accidentally use the `given` from Mockito.
To make things interesting, you can change the `@WunderBarApiConsumer` annotation to `@WunderBarApiConsumer(level = INTEGRATION)` (or simply change the test name to end with `IT`, short for Integration Test): WunderBar now starts a mock server exposing the behavior you just stubbed, i.e. it will reply to your real http request with the proper product http response.
No code changes needed, and now it fully tests your REST or GraphQL client annotations and (de)serialization of your POJOs.This is nice, but as a welcome side effect, it _records_ the requests and responses you need for your code to work, and saves it in a `wunder.bar` file (Behavior ARchive).
Give this file to your API provider, so they can check if their service complies to your requirements.
You can even deploy this file, together with your other maven artefacts, e.g. by using the `attach-artifact` goal of the `build-helper-maven-plugin`; for an example, look at the pom in the https://github.com/t1/wunderbar/blob/trunk/demo/order/pom.xml[`demo/order`] submodule.
Note that you'll get a `Failed to install artifact` error when running `mvn clean install -DskipTests`, because you skip the tests that generate the `bar` files, so the plugin can't attach them.
Most of the time, executing `clean` is not necessary (and slows your build down); and running `install` before releasing your software is only useful for libraries, not for applications; simply run `mvn package -DskipTests` instead.
If you still need the `install`, you can also pass `-Dbuildhelper.skipAttach`.You can disable recording for one stub by calling `withoutRecording`, e.g. when testing the error handling in your consumer code, so this interaction is not an expected behavior of the API provider.
=== Generating Test Data (Consumer)
Generating good test data can become tedious; numbers should be rather small, positive, and (above all) unique, so the value are easy to handle and recognize.
WunderBar provides an extensible mechanism to generate test data that fulfills these requirements, using a simple counter starting for every test.
And it logs the values it generated for what purpose, so you can easily find the source for a value.
You can generate a value for a field or parameter by annotating it as https://github.com/t1/wunderbar/blob/trunk/junit/src/main/java/com/github/t1/wunderbar/junit/consumer/Some.java[`@Some`].[source,java]
----
@WunderBarApiConsumer
class ProductResolverTest {
@Test void shouldUpdateProduct(@Some int newPrice) {
given(/*...*/).returns(product.withPrice(newPrice));var resolvedProduct = resolver.productWithPriceUpdate(item, newPrice);
then(resolvedProduct).usingRecursiveComparison()
.isEqualTo(product.withPrice(newPrice));
}
}
----`@Some int newPrice` could also be a field, and it would work exactly the same.
Note how the `newPrice` parameter is used throughout the test.
This makes it easier for the reader of your code to understand which test value is which.
Sometimes, you'll want to provide some constant value; as they should be distinct from the generated values, use values below 100, which is where `@Some` will start counting from by default.
And the generator will fail, if you generate values beyond `Short.MAX_VALUE` = `32767` = `2^15^-1` = `0x7FFF`; generating a `byte` fails sooner, obviously.Out-of-the-box, you can generate the primitive types `byte`, `char`, `short`, `int`, `long`, `float`, `double` (or their wrapper types `Integer`, etc.), and some basic types like `String`, `URI`, `LocalDateTime`, etc. (see https://github.com/t1/wunderbar/blob/trunk/junit/src/main/java/com/github/t1/wunderbar/junit/consumer/SomeBasics[here] for the full list), but obviously not `boolean`: they can hardly be considered unique.
You can change the starting point by calling `SomeBasics#reset`, e.g. in a `@BeforeEach`.
You can also generate `List` or `Set`, which will contain exactly one generated element, and instances of arbitrary classes, which will recursively generate values for every field.To generate your own data, e.g., `@Some Product product`, you can register your own generator class: `@Register(SomeProducts.class)`, where `SomeProducts` implements https://github.com/t1/wunderbar/blob/trunk/junit/src/main/java/com/github/t1/wunderbar/junit/consumer/SomeData.java[`SomeData`].
The `@Some` annotation takes an optional list of String tags that are passed to custom generators, along with the `AnnotatedElement` location, so it can fine-control what data it should generate, e.g. to generate `invalid` objects.You can also inject an instance of `SomeGenerator` into your generator's constructor to dynamically generate other values you depend on, or to look up the location or tags of an actual value.
For a full example see https://github.com/t1/wunderbar/blob/trunk/junit/src/test/java/test/consumer/SomeProduct.java[here].== 2 Minute API Provider Intro
When you implement an API (i.e. you provide it), you can load a suite of tests that has been stored in a `wunder.bar` file, and run them against your service:
[source,java]
----
@WunderBarApiProvider(baseUri = "http://localhost:8080")
class ConsumerDrivenAT {
@TestFactory DynamicNode orderTests() {
return findTestsIn("wunder.bar");
}
}
----There are several ways to load `bar` files; e.g., you can also load them from maven coordinates.
See the public methods in the https://github.com/t1/wunderbar/blob/trunk/junit/src/main/java/com/github/t1/wunderbar/junit/provider/WunderBarTestFinder.java[`WunderBarTestFinder`] class for details.The requirements will be more specific than your service, but that's a good thing: thankfully, your service will be lenient in some cases; e.g. it accepts different content type encodings, like `ISO-8859-1` or `utf-8`.
In this way, a client can change some details of its technical requirements, e.g. by requesting a different encoding or even content type (e.g. `json` instead of `xml`); as long as your service supports it, the tests continue to pass.
And if it doesn't support it, it will show up as soon as the new version of the bar file runs.If the test data in your service is static and matches the expectations of your clients/consumers, that's it!
But to be honest, managing test data is generally a nastily complex issue, and WunderBar can help, but can't make it go away completely.=== Managing Test Data (Provider)
Consumer Driven Contract testing is about the _structure_ of the data, the API.
But the requests and responses in a `bar` file also contain some more or less random _data_ itself.
The most common reflex is to create exactly that data in your test system, which is okay as long as the data is very static.
But test data often changes or is even deleted for various reasons: some data simply times out, other data is changed by manual as well as automated tests, etc.
This demands coordination between different teams, resulting in high effort and brittle tests: they sporadically break without exposing a real bug anywhere but in this communication between people.Your tests will be much more maintainable, if set up (and maybe clean up) data in your service to match the consumers' requirements, i.e. mostly putting the expected response into your system.
You can do so by using some mutating APIs of your service, or by storing and deleting the data directly into your database, or by defining an extra test backdoor API for your service:
either way, you'll need do this kind of test setup before every test in the BAR (and maybe some cleanup thereafter).
To do so, just define a method, annotated as https://github.com/t1/wunderbar/blob/trunk/junit/src/main/java/com/github/t1/wunderbar/junit/provider/BeforeInteraction.java[`@BeforeInteraction`]footnote:[JUnit invokes the standard JUnit `@Before/AfterEach` methods only once for every test method, not for every test in a `DynamicNode`. WunderBar also calls methods annotated as https://github.com/t1/wunderbar/blob/trunk/junit/src/main/java/com/github/t1/wunderbar/junit/provider/BeforeDynamicTest.java[`@BeforeDynamicTest`] / https://github.com/t1/wunderbar/blob/trunk/junit/src/main/java/com/github/t1/wunderbar/junit/provider/AfterDynamicTest.java[`@AfterDynamicTest`]; the difference is that, in some cases, there can be several subsequent interactions within one dynamic test, so methods with `Before/AfterDynamicTest` work on Lists.], taking a single parameter of type https://github.com/t1/wunderbar/blob/trunk/lib/src/main/java/com/github/t1/wunderbar/junit/http/HttpRequest.java[`HttpRequest`], https://github.com/t1/wunderbar/blob/trunk/lib/src/main/java/com/github/t1/wunderbar/junit/http/HttpResponse.java[`HttpResponse`], or https://github.com/t1/wunderbar/blob/trunk/lib/src/main/java/com/github/t1/wunderbar/junit/http/HttpInteraction.java[`HttpInteraction`] (which basically just bundles a request and response).In addition to storing the data in your system, you can also manipulate the request or the expected response by returning an `HttpInteraction`, `HttpRequest`, or `HttpResponse` from your `BeforeInteraction` method to modify the interaction, e.g. to replace the dummy credentials from the bar file (xref:credentials[see below]) with real credentials your service will accept.
The https://github.com/t1/wunderbar/blob/trunk/lib/src/main/java/com/github/t1/wunderbar/junit/http/HttpRequest.java[`HttpRequest`] and https://github.com/t1/wunderbar/blob/trunk/lib/src/main/java/com/github/t1/wunderbar/junit/http/HttpResponse.java[`HttpResponse`] classes help here with a bunch of convenient methods.This works nicely when reading data, but you'll need more for mutating operations; e.g. when a test creates a record in a database, it most often will also generate something like a primary key, which will not match the key in the expected response.footnote:[You actually could create the data in a setup method, manipulate the expected response accordingly, and rely on your service being idempotent, so the real call will return the same data, but this is not only more work but also contra-intuitive. There's a better way.]
To manipulate the expected response to match a value from the actual response, write a method annotated as https://github.com/t1/wunderbar/blob/trunk/junit/src/main/java/com/github/t1/wunderbar/junit/provider/AfterInteraction.java[`@AfterInteraction`].
You can't return a request here anymore (as it's already done), but get the actual response with a https://github.com/t1/wunderbar/blob/trunk/junit/src/main/java/com/github/t1/wunderbar/junit/provider/Actual.java[`@Actual`] annotated `HttpResponse` parameter and use that to manipulate the expected result as needed.You can also filter the tests to actually run, by annotating a method as https://github.com/t1/wunderbar/blob/trunk/junit/src/main/java/com/github/t1/wunderbar/junit/provider/BeforeDynamicTest.java[`@BeforeDynamicTest`], and returning the `List` with tests removed as you wish.
You could even add your own, e.g. by duplicating (and then probably modifying) an existing one.Writing your acceptance tests in this way makes your testing more robust, as you don't have to agree with the consumers of your APIs on any volatile and intransparent assumptions about the test data, e.g. what ids or data fields result in what behavior.
For a fully running example, see the demo https://github.com/t1/wunderbar/blob/main/demo/product/src/test/java/test/acceptance/ConsumerDrivenAT.java[ConsumerDrivenAT].[#credentials]
== Credentials`bar` files never contain the secrets of a real `Authorization` header footnote:[They used to say that the username was a secret, too, but when you use good passwords (i.e. really random and really long), this is not necessary anymore, but it makes life so much easier to see the username.].
They could contain random values for integration tests, without adding any benefit; for system-level tests (xref:system-level-tests[see below]) against a real service, the interactions would even contain real credentials.
So WunderBar only writes dummy values instead.For a GraphQL client, you can use the `@AuthorizationHeader` annotation to read the configuration from an MP Config property; but you don't have to actually provide those for an integration test, as they won't be written anyway; a dummy value will be written instead.
OTOH, a `@Header(name = "Authorization")` works normally (but won't be written either).On the API provider side, the acceptance test has to replace this value with real credentials, e.g. by returning a modified `HttpRequest` in a `@BeforeInteraction` method.
== Full Dependency Injection
Using the `@SystemUnderTest` annotation performs only a very limited form of dependency injection.
For more complex dependency requirements, it may be appropriate to use, e.g., https://github.com/weld/weld-junit/blob/master/junit5/README.md[`weld-junit5`] as a fully blown CDI testing environment.
To do so, do the following steps:1. add a `test` scope dependency on `org.jboss.weld:weld-junit5`,
2. annotate your test class with `@EnableWeld` _after_ (this is important) the `@WunderBarApiConsumer` annotation,
3. instead of `@SystemUnderTest`, use the CDI `@Inject` annotation, and
4. build a `WeldInitiator` with your classes, and for the services, add a mock bean with a _delayed_ `create` producer of the WunderBar-mocked service field.This sums up like this:
[source,java]
----
@WunderBarApiConsumer
@EnableWeld
class ProductsResolverWeldIT {
@Service Products products;
@Inject ProductsResolver resolver;@WeldSetup
WeldInitiator weld = WeldInitiator.from(ProductsResolver.class, Products.class)
.addBeans(MockBean.builder().types(Products.class).create(ctx -> products).build())
.build();
}
----In this way, WunderBar produces the service proxy, and Weld can inject it into your system under test.
For a complete example, take a look at https://github.com/t1/wunderbar/blob/main/demo/order/src/test/java/test/graphql/ProductsResolverWeldIT.java[`ProductsResolverWeldIT`].[#system-level-tests]
== `SYSTEM` Level TestsTo go one step further than integration tests, you can use the test level `SYSTEM`, maybe by renaming your test class suffix from `IT` to `ST`.
This means that you actually deploy your service to a full environment, often called 'dev stage'.
Then your service needs to call a running instance of the target systems' API.
WunderBar provides the https://search.maven.org/artifact/com.github.t1/wunderbar.mock.server[`wunderbar-mock-server`] `war` artifact that you can deploy so your system under test service can reach it and configure your service to do so; no code changes needed.
Configure the `@Service#endpoint` to the address of this mock service.
If you call a `given` on the stub that's injected into your test, WunderBar prepares this// TODO finish documentation
You can use system-level tests to test a real system, as long as you only test with the data that exists in that service, as calling `given` will try
// TODO finish documentation
== Full Documentation
The full documentation is in the JavaDoc, mainly in the https://github.com/t1/wunderbar/blob/trunk/junit/src/main/java/com/github/t1/wunderbar/junit/consumer/WunderBarApiConsumer.java[`@WunderBarApiConsumer`] annotation, the https://github.com/t1/wunderbar/blob/trunk/junit/src/main/java/com/github/t1/wunderbar/junit/consumer/Level.java[`Level`] enum and the https://github.com/t1/wunderbar/blob/main/junit/src/main/java/com/github/t1/wunderbar/junit/consumer/WunderbarExpectationBuilder.java[`WunderbarExpectationBuilder`] for the API consumer (client) side and in the https://github.com/t1/wunderbar/blob/trunk/junit/src/main/java/com/github/t1/wunderbar/junit/provider/WunderBarApiProvider.java[`@WunderBarApiProvider`] annotation and the https://github.com/t1/wunderbar/blob/trunk/junit/src/main/java/com/github/t1/wunderbar/junit/provider/WunderBarTestFinder.java[`WunderBarTestFinder`] for the API provider (server) side.
The `demo` module contains two example projects: `order` consumes an API that the `product` service provides.
Both in REST and GraphQL and on all test levels.If you have further questions, don't hesitate to ask questions on Stack Overflow tagged with https://stackoverflow.com/questions/tagged/wunderbar[wunderbar].
Contributions are also very welcome, of course: start discussions, open issues, add comments, share it online or offline, and if you like it, give it a star on GitHub, please 😁