-
pact-enter
to create the pact mock -
extend-enter
to set up the@ExtendWith
-
template-enter
for the JUnit template tests -
shift-cmd-T to open or create tests
-
cmd-opt-T-6 to wrap a block in try-catch
-
fn-cmd-F1 to switch screen mirroring on and off
- Print these instructions!
- Run
./prep-demo
script, which will delete the contract tests and initialise the podman container environment. It also clears the database if the architecture recorder is running.
- Pause any backup software
- Turn on Mac Focus
- Quit email and other messaging tools
- Make terminal and IDE fonts huge
- Make browser font huge
- Reduce screen resolution to 1920 x 1080
- Sort out web conference green screen if necessary
- Get an iPad with a timer running
- Use ctrl↑ to open mission control and inspect the desktops; make one desktop for slides, one for the demo, and one for the stream (if remote). To swipe between desktops, use ctrl→ and ctrl←
Open two IDEs, one knitter, and farmer (cold person also has code, but we don't want the clutter). Make each window big, but not full-screen, by clicking Option-GreenButton.
Open a terminal within each IDE (or two OS terminals).
- If this is the first time you are running this, build/install the observer extension:
cd observer-extension ./mvnw install
- Start the architecture recorder:
cd architecture-recorder quarkus dev
- Start the cold person:
cd cold-person quarkus dev
It may be useful to clear all architecture information, or just the historical interactions.
curl -i -X POST http://localhost:8088/recorder/clearall
curl -i -X POST http://localhost:8088/recorder/clearinteractions
- Start the cold-person service with
quarkus dev
. Visit http://localhost:8080. The app has a React front end and a Quarkus back end, stitched together and bridged by Quinoa. - Try and do an order. Nothing will happen; there are no other services.
- Start the knitter service. It should appear on the front end.
- Try and do an order. Nothing will happen; we need wool.
- Start the farmer service. It should appear on the front end.
- Do an order for a white sweater. It should succeed, and an set of orders should appear.
- We've had to do quite a lot of starting of services, just to see if our app works... and this is a trivial application. Microservices are hard!
- Show the tests. Because of course there are tests, we're responsible developers.
- Run the back-end tests in IntelliJ (they're also running continuously in Quarkus)
- Refactor the
Skein
in the farmer app. Colour is a British spelling. To refactor,shift-f6
oncolour
variable, change it to ‘color’. Getters and setters will update too. . - Sense check. Run the Java tests (all working), all working in all services.
- Visit the web page again. It's all going to work, right, because the tests all worked?
- Shouldn't the unit tests have caught this? Look at
WoolResourceTest
. Because we're using the object model, our IDE automatically refactoredgetColour
togetColor
. We could have done more hard-coding in the tests, but that's kind of icky, and the IDE might have refactored the hard-coded strings, too. If we were to lock the hard-coded strings and say they can't be changed ... well, that's basically a contract. But it's only on one side, with no linkage to the other side, so it's pretty manual and error prone. Look how much of this is duplicated; the interface FarmerService/Farmer, the WoolOrder domain object … Not really any clear indication which of this should be ‘locked’ and which can change.
- How can we fix this? The app is broken, but the tests are all green. This is where contract tests give that extra validation to allow us to confirm assumptions.
- Pact is consumer-driven contract testing, so we start with the consumer. It's a bit like a TDD principle, start with the expectations. Open the
knitter
project. - In a terminal, run
quarkus extension add pact-consumer
- Add the tests
- If you're in a hurry, use the git history to recreate the contract tests. Rollback any pom changes and
SweaterResourceContractTest
from the git history. - Otherwise, copy the
SweaterResourceTest
and use it as the starting point. The test method stays exactly the same, because we're trying to validate the behaviour of our knitter code. - The mocking logic is a bit different. Delete both the mock injection and the
BeforeEach
pact-enter for the following live template:
@Pact(provider = "farmer", consumer = "knitter") public V4Pact createPact(PactDslWithProvider builder) { var headers = Map.of(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON); // Here we define our mock, which is also our expectations for the provider // This defines what the body of the request could look like; we are generic and say it can be anything that meets the schema var woolOrderBody = LambdaDsl.newJsonBody(body -> body .stringType("colour") .numberType("orderNumber") ).build(); var woolBody = LambdaDsl.newJsonBody(body -> body.stringValue("colour", "white")).build(); return builder .uponReceiving("post request") .path("/wool/order") .headers(headers) .method(HttpMethod.POST) .body(woolOrderBody) .willRespondWith() .status(Status.OK.getStatusCode()) .headers(headers) .body(woolBody) .toPact(V4Pact.class); }
- If you're in a hurry, use the git history to recreate the contract tests. Rollback any pom changes and
- Finally, we need to add some extra annotations. extend-enter on the class declaration to add
@ExtendWith(PactConsumerTestExt.class)
@PactTestFor(providerName = "farmer", port = "8096")
- Show the
SweaterResourceTest
and then compare the two tests. Explain the differences are because Pact acts both as a mock and a validator of all possible values. - Restart the tests. A json contract has appeared in
knitter/target/pacts
- The test should pass, we're the consumer, we made assumptions about how the provider should behave. But are those assumptions correct? Now is when we find out!
- Copy the pact from the knitter to share it to the farmer:
publish-contracts.sh
. Normally this would be done by automatically checking it into source control or by using a pact broker.
- Add the pact provider dependency by running
quarkus extension add pact-provider
(or by restoringfarmer/pom.xml
from history). - Open
WoolResource
- Type shift-cmd-T and create a new test. Name it
WoolResourceContractTest
. - Type template-enter to insert the JUnit template test.
- Run the Java tests, show the failure, explain how it could be fixed by negotiating a contract change or changing the source code.
- What happens if we order other sweater colours? In the UI, order a white sweater, a grey sweater, a brown sweater, a black sweater ... all should work fine. Then try a pink sweater - it will come back as white! This is a business process flaw we've identified. We don't have any dyer in the flow, so the farmer is restricted to colours of sheep.
- We were too vague in our contract. We actually said in the contract any colour would give a white sweater. Update the knitter tests so that the test requests a grey sweater, and gets back a grey sweater. The test will fail, because we need to update the mock.
- Update the mock to pass in grey, and change
stringType
to
.stringValue("colour", "pink")
The knitter tests should be passing. (Normally we would build up the tests, but to keep it simple, we will just change the test.) 2. Now publish the tests, and we have the failure. 3. Now repeat, but for pink. The tests will fail for the provider. 3. If you do want to add both tests, you can copy the existing pact and test methods, and change the colours in the copy. You will also need to add
@PactTestFor(pactMethod = "buyingAPinkSweater")
onto the test method. (By default, Pact will only stand up the first @Pact
for the right provider.)
- So we have a failing test, but what's the right fix? Fallback to white isn't right, there should be some kind of error.
- Update the Pact mock/contract in the tests to return a 404 (and not return any headers). At this point tests may start failing.
.willRespondWith()
.status(404)
// .headers(headers)
// .body(woolBody)
.toPact(V4Pact.class);
- We think we should have a
418
, not a white sweater.418
isI'm a teapot
, maybe not the right code, but it's my code so I can return what I want. Also, it keeps behaviour of the different services distinct. - Show the exception mapper to turn
NotFoundException
s into 418s. It's in theNotFoundExceptionHandler
. - Update the tests to expect a
418
.
@Test
public void testSweaterEndpointForWhiteSweater() {
SweaterOrder order = new SweaterOrder("pink", 100);
given()
.contentType(ContentType.JSON)
.body(order)
.when()
.post("/sweater/order")
.then()
.statusCode(418);
// .extract().as(Sweater.class);
// assertEquals("pink", sweater.getColour());
}
- Update the implementation in
SweaterResource
to wrap the invocation in atry
and catch. Shortcut is cmd-opt-T-6.
} catch (Exception e) {
throw new NotFoundException(order.getColour());
}
- The tests should pass. All good, except ...
- ... publish the tests, and they fail on the other side.
- Update the code to return null instead of white as the fallback, and ... they should still fail.
- In this case the right thing to do is to update the contract to return
204
, and update theSweaterResource
implementation to instead to a null check. The implementation would be to add a
if (skein == null) {
check in SweaterResource
and then throw an exception from the null check body.