Proper interactions logging and sequence diagrams

In the last post adding a stub we introduced a Wiremock instance that pretended to be a third party. Now we have to fix the yatspec output show this interaction correctly.

The first thing that is not right is the way we are logging interactions. The test is logging three lines: “Request From A_user to HelloWorldApp”, “Response” and “Response From HelloWorldApp To A_user”.

bad_interaction_logging

There is a number of things wrong with this logging:

  1. We are not logging interactions with our third party. It is also not shown in the sequence diagram.
  2. “Request From A_user to HelloWorldApp” is a horrible name. Also it’s not logging properly the contents of the request. For example, if we had a header it would not be rendered.
  3. “Response” should not be here.
  4. “Response From HelloWorldApp To A_user”: similar to 2. Bad name and bad rendering (we can’t see the body).

We will fix these problems. We will start with number 3.

Carrying over state

In most tests we have to face one way or another the fact that there is a state related with the testing fases: given (setup), when (execution) and then (verification). Usually we have to put in place state during the given to be used during the execution. Often we also have to store the execution information (returned values and/or interactions) and use it to assert something in the verification.

In the example test that we have written, Wiremock deals with the state of the Givens, so that part is easy. The problem is the result coming back from the When. Right now we are storing it in the interesting givens so we can retrieve it in the thens:

private static CapturedInputAndOutputs whenWeMakeARequestTo(CapturedInputAndOutputs capturedInputAndOutputs, HttpGet request) throws IOException {
    capturedInputAndOutputs.add(format("Request from %s to %s", "a_user", "helloWorldApp"), request);
    HttpResponse response = HttpClientBuilder.create().build().execute(request);
    capturedInputAndOutputs.add("response", response);
    capturedInputAndOutputs.add(format("Response from %s to %s", "helloWorldApp", "a_user"), response.getStatusLine().toString());
    return capturedInputAndOutputs;
}

CapturedInputsAndOutputs are part of the Yatspec API and should only be used to interact with Yatspec. It is not meant to be a “bag of mutable state” used to communicate the different moving parts of the test.

We are going to define our own object to hold this state so it can be passed along the different parts of the test.

public class TestState {
    private Map<String, Object> dataToBeAssertedOn = new HashMap<>();

    public TestState add(String key, Object value){
        dataToBeAssertedOn.put(key, value);
        return this;
    }

    public Object get(String key){
        return dataToBeAssertedOn.get(key);
    }
}
public class Thens {
    private TestState testState;
    public Thens(TestState testState) { testState = testState; }

    public StateExtractor<Integer> statusCode() {
        return capturedInputAndOutputs ->
                ((HttpResponse)testState.get("response")).getStatusLine().getStatusCode();
    }

    public StateExtractor<String> body() {
        return capturedInputAndOutputs -> EntityUtils.toString(((HttpResponse)testState.get("response")).getEntity());
  We are not logging interactions with our third party. It is also not shown in the sequence diagram
  }
}
public class Whens {
    private TestState testState;

    public Whens(TestState testState) {
        this.testState = testState;
    }

    private CapturedInputAndOutputs whenWeMakeARequestTo(CapturedInputAndOutputs capturedInputAndOutputs, HttpGet request) throws IOException {
        capturedInputAndOutputs.add(format("Request from %s to %s", "a_user", "helloWorldApp"), requestToString(request));
        HttpResponse response = HttpClientBuilder.create().build().execute(request);
        testState.add("response", response);
        capturedInputAndOutputs.add(format("Response from %s to %s", "helloWorldApp", "a_user"), response.getStatusLine().toString());
        return capturedInputAndOutputs;
    }
}

We had to make the Whens and Thens non static, which affected the Test class:

public class StarWarsTest extends AbstractAcceptanceTest {
    private final TestState testState = new TestState();
    private final Whens weMake = new Whens(testState);
    private final Thens the = new Thens(testState);

    @Test
    public void shouldTalkAboutLukeSkywalkerByDefault() throws Exception {
        given(theStarWarsServiceKnowsAboutLuke());
        when(weMake.aGetRequestTo("http://localhost:8080/starWarsCharacter"));
        then(the.statusCode(), is(200));
        then(the.body(), is("{\"Description\": \"Luke Skywalker is a Human from Tatooine\"}"));
    }
}

Now the test is no longer rendering the undesired line:

removed_bad_logging_line.png

repo: https://github.com/obprado/EmbeddedJetty/tree/2043608da40aae81c5c3cc409e7a5fd378f86a6b

changeset: https://github.com/obprado/EmbeddedJetty/commit/2043608da40aae81c5c3cc409e7a5fd378f86a6b

Listening to Wiremock

Lets review our TODOs:

  1. We are not logging interactions with our third party. It is also not shown in the sequence diagram.
  2. “Request From A_user to HelloWorldApp” is a horrible name. Also it’s not logging properly the contents of the request. For example, if we had a header it would not be rendered.
  3. “Response” should not be here.
  4. “Response From HelloWorldApp To A_user”: similar to 2. Bad name and bad rendering (we can’t see the body).

Now we are going to fix number 1.

Out Givens class is the one that knows what are the expected interactions. Therefore it makes sense that it is also responsible of recording those interactions in the capturedInputsAndOutputs.

Wiremock offers the possibility of listening to the requests it receives and executing a callback. Both the Wiremock server and the capturedInputsAndOutputs live in the AbstractAcceptanceTest class (and therefore in every acceptance test) because it inherits from TestState in the Bodart library (do not confuse with our own TestState class).

If we want the Givens to use that functionality, we have to expose it in AbstractAcceptanceTest and pass it to the Givens. To do that we will have to make Givens non-static.

Exposing functionality in the AbstractAcceptanceTest class:

public void addToCapturedInputsAndOutputs(String key, Object capturedStuff){
    testState().capturedInputAndOutputs.add(key, capturedStuff);
}

public void listenToWiremock(RequestListener listener){
    wireMockServer.addMockServiceRequestListener(listener);
}

Non static Givens:

private final TestState testState = new TestState();
private final Whens weMake = new Whens(testState);
private final Givens theStarWarsService = new Givens(this);
private final Thens the = new Thens(testState);

@Test
public void shouldTalkAboutLukeSkywalkerByDefault() throws Exception {
    given(theStarWarsService.knowsAboutLuke());
    when(weMake.aGetRequestTo("http://localhost:8080/starWarsCharacter"));
    then(the.statusCode(), is(200));
    then(the.body(), is("{\"Description\": \"Luke Skywalker is a Human from Tatooine\"}"));
}

Making the Givens non-static and recording a wiremock listener:

public class Givens {
    private AbstractAcceptanceTest abstractAcceptanceTest;

    public Givens(AbstractAcceptanceTest abstractAcceptanceTest) {
        this.abstractAcceptanceTest = abstractAcceptanceTest;
    }

    private void registerListener() {
        abstractAcceptanceTest.listenToWiremock(this::recordTraffic);
    }

    private void recordTraffic(Request request, Response response) {
        abstractAcceptanceTest.addToCapturedInputsAndOutputs("request from helloWorldApp to swapi", request);
        abstractAcceptanceTest.addToCapturedInputsAndOutputs("response from swapi to helloWorldApp", response);
    }

    public GivensBuilder knowsAboutLuke() {
        registerListener();
        return interestingGivens -> {
                 ...
    }
}

Now the yatspec output is slightly better.

addedthirdpartyinteractions

repo: https://github.com/obprado/EmbeddedJetty/tree/74898a88093c1cc54c64da0a115c8b19f7d3f5a7

changeset: https://github.com/obprado/EmbeddedJetty/commit/74898a88093c1cc54c64da0a115c8b19f7d3f5a7

Proper formatting of requests and responses

We are closer to be done with our TODOs list:

  1. We are not logging interactions with our third party. It is also not shown in the sequence diagram.
  2. “Request From A_user to HelloWorldApp” is a horrible name. Also it’s not logging properly the contents of the request. For example, if we had a header it would not be rendered.
  3. “Response” should not be here.
  4. “Response From HelloWorldApp To A_user”: similar to 2. Bad name and bad rendering (we can’t see the body).

We will tackle 2 & 4 now.

To render this properly we are going to need to do some transformations on the request/response data we have to make Yatspec and PlantUml happy.

I have a but of boilerplate code from a different project that will help with that. It will be under the packages “acceptancetests/yatspec” and “httpclient”.

 

New code in AbstractAcceptanceTest to support recording the traffic with third parties properly:

public void recordTraffic(Request request, Response response, String sourceApplication, String destinyApplication) {
    RequestResponse requestResponse = requestResponse(sourceApplication, destinyApplication);
    addToCapturedInputsAndOutputs(requestResponse.request(), toYatspecString(request));
    addToCapturedInputsAndOutputs(requestResponse.response(), toYatspecString(response));
}

public RequestResponse requestResponse(String from, String to) {
    return requestAndResponsesFormatter.requestResponse(from, to);
}

And now we use it in the Givens, where we know the name of the third party. Right now there is only one, but as an application grows it is normal for it to need several ones.

private void recordTraffic(Request request, Response response) {
    abstractAcceptanceTest.recordTraffic(request, response, AbstractAcceptanceTest.APPLICATION_NAME, THIRD_PARTY_NAME);
}

Modifying the Whens so they record it properly:

private CapturedInputAndOutputs whenWeMakeARequestTo(CapturedInputAndOutputs capturedInputAndOutputs, HttpGet request) throws IOException {
    capturedInputAndOutputs.add(format("Request from %s to %s", CALLER, AbstractAcceptanceTest.APPLICATION_NAME), httpclient.Request.toNiceRequestForYatspec(request));
    HttpResponse response = HttpClientBuilder.create().build().execute(request);
    httpclient.Response domainResponse = httpclient.Response.fromApacheResponse(response);
    testState.add("response", domainResponse);
    capturedInputAndOutputs.add(format("Response from %s to %s", AbstractAcceptanceTest.APPLICATION_NAME, CALLER), domainResponse);
    return capturedInputAndOutputs;
}

Note this bit about the fromApacheResponse method:

public static Response fromApacheResponse(HttpResponse response) {
    try {
        return new Response(EntityUtils.toString(response.getEntity()), response.getStatusLine().getStatusCode(), response.getProtocolVersion().toString(), Headers.fromApacheHeaders(response.getAllHeaders()));
    } catch (IOException exception) {
        throw new RuntimeException("TODO make sure that the response has a proper format", exception);
    }
}

Response.getEntity returns an inputStream, and EntityUtils.toString() consumes it. It cannot be consumed again afterwards. Therefore, it is important that from this point onwards we always we keep the Response object if we need to access the body from several places.

finalsequencediagramforblogpost

Now if we hover over any request or response we will see a popup with it’s contents

popup

repo: https://github.com/obprado/EmbeddedJetty/tree/b7d553f39a13221b4fae7b0eeb1141cb44cdd89f

changeset: https://github.com/obprado/EmbeddedJetty/commit/b7d553f39a13221b4fae7b0eeb1141cb44cdd89f

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s