Extending and refactoring Yatspect suite

After Automatic Generation of Sequence Diagrams with Yatspec, we are going to add a second test and a bit of functionality to the project. This will come handy later to show other Yatspec tools.

New functionality: Star wars characters

We are going to add an endpoint that gives us information about Star Wars characters. We will be sourcing the data from swapi, documented in https://swapi.co/documentation.

Following the Red-Green-Refactor cycle, we add a new failing test. We will refactor the duplication in the tests later.

@RunWith(SpecRunner.class)
public class StarWarsTest extends TestState implements WithCustomResultListeners {

    [...]

    @Test
    public void shouldTalkAboutLukeSkywalkerByDefault() throws Exception {
        when(weMakeAGetRequestTo("http://localhost:8080/starWarsCharacter"));
        thenTheResponseCodeIs200AndTheBodyIs("{\"Description\": \"Luke Skywalker is a Human from Tatooine\"}");
    }
   
    [...]
}

When we run it, it is red as expected.

org.junit.ComparisonFailure:
Expected :200
Actual   :404

Now we add the endpoint and a quick implementation. Pom.xml:

<dependencies>

    [...]

    <!-- JSON parsing and rendering -->
    <dependency>
        <artifactId>json-path</artifactId>
        <groupId>com.jayway.jsonpath</groupId>
        <version>2.2.0</version>
    </dependency>

    [...]  

</dependencies>

Adding a new Servlet to manage our endpoint:

private ServletContextHandler helloServletHandler() {
    ServletContextHandler servletHandler = new ServletContextHandler();
    servletHandler.addServlet(new ServletHolder(new HelloWorldHttpServlet()), "/hello");
    servletHandler.addServlet(new ServletHolder(new StarWarsCharacterHttpServlet()), "/starWarsCharacter");
    return servletHandler;
}

and now implementing it:

public class StarWarsCharacterHttpServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        HttpGet lukeRequest = new HttpGet("http://swapi.co/api/people/1/");
        CloseableHttpResponse lukeResponse = HttpClientBuilder.create().build().execute(lukeRequest);

        String body = EntityUtils.toString(lukeResponse.getEntity());
        DocumentContext lukeJsonDocument = JsonPath.parse(body);
        String lukeName = lukeJsonDocument.read("$.name");

        String lukeSpeciesUrl = lukeJsonDocument.read("$.species[0]");
        String lukeHomeworldUrl = lukeJsonDocument.read("$.homeworld");

        HttpGet lukeSpeciesRequest = new HttpGet(lukeSpeciesUrl);
        CloseableHttpResponse lukeSpeciesResponse = HttpClientBuilder.create().build().execute(lukeSpeciesRequest);
        String lukeSpeciesBody = EntityUtils.toString(lukeSpeciesResponse.getEntity());
        String lukeSpecies = JsonPath.parse(lukeSpeciesBody).read("$.name");

        HttpGet lukeHomeworldRequest = new HttpGet(lukeHomeworldUrl);
        CloseableHttpResponse lukeHomeworldResponse = HttpClientBuilder.create().build().execute(lukeHomeworldRequest);
        String lukeHomeworldBody = EntityUtils.toString(lukeHomeworldResponse.getEntity());
        String lukeHomeworld = JsonPath.parse(lukeHomeworldBody).read("$.name");


        response.getWriter().print(format("{\"Description\": \"%s is a %s from %s\"}", lukeName, lukeSpecies, lukeHomeworld));
        response.setStatus(200);
    }
}

We run the tests and… it works!

Now, there are several things to improve here:

  1. We are running our acceptance test against a real prod server. This is a luxury we can’t always afford.
  2. The acceptance test is veeeeery slow. It takes several seconds to execute, whereas the helloWorld test takes around half a second. This is probably related with point (1).
  3. Both test and prod code need refactoring, code duplication smells.
  4. Acceptance tests wording needs improvement.
  5. The sequence diagram is need to show appropriate data (for example, the response should show the body)
  6. The sequence diagram should show the interaction between helloWorldApp and SWAPI.
  7. We are hardcoding Luke Skywalker information, we will enrich the api to offer something more.

We commit and push the code and start refactoring.

Repo: https://github.com/obprado/EmbeddedJetty/tree/b04b8359e29745c739aff786b8ee5dd03890cb30

Refactoring test and prod code

Refactor time!

The tests have several responsibilities mixed right now, making them quite messy:

  1. General integration test work (start up the server, stop the server generate sequence diagram,…)
  2. When/then work (low level code for creating http requests, extracting data from the response)
  3. Test specific work (defining the endpoint to hit and the expected values for the results)

Point (1) is extracted in a superclass:

@RunWith(SpecRunner.class)
public abstract class AbstractAcceptanceTest extends TestState implements WithCustomResultListeners {

    private ExampleApp exampleApp = new ExampleApp();

    @Before
    public void setUp() throws Exception {
        exampleApp.run();
    }

    @After
    public void tearDown() throws Exception {
        exampleApp.stop();
        capturedInputAndOutputs.add("Sequence Diagram", generateSequenceDiagram());
    }

    private SvgWrapper generateSequenceDiagram() {
        return new SequenceDiagramGenerator().generateSequenceDiagram(new ByNamingConventionMessageProducer().messages(capturedInputAndOutputs));
    }

    @Override
    public Iterable<SpecResultListener> getResultListeners() throws Exception {
        return singletonList(new HtmlResultRenderer()
                .withCustomHeaderContent(SequenceDiagramGenerator.getHeaderContentForModalWindows())
                .withCustomRenderer(SvgWrapper.class, new DontHighlightRenderer<>()));
    }
}

Point (2) is extracted in “Then” and “When” helper classes.

public class Whens {

    public static ActionUnderTest weMakeAGetRequestTo(String uri) {
        return (interestingGivens, capturedInputAndOutputs) -> whenWeMakeARequestTo(capturedInputAndOutputs, new HttpGet(uri));
    }

    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;
    }
}
public class Thens {

    public static StateExtractor<Integer> theStatusCode() {
        return capturedInputAndOutputs ->
                capturedInputAndOutputs.getType("response", HttpResponse.class).getStatusLine().getStatusCode();
    }

    public static StateExtractor<String> theBody() {
        return capturedInputAndOutputs -> EntityUtils.toString(capturedInputAndOutputs.getType("response", HttpResponse.class).getEntity());
    }
}

And point (3) remains in the test classes, but much cleaner:

public class StarWarsTest extends AbstractAcceptanceTest {

    @Test
    public void shouldTalkAboutLukeSkywalkerByDefault() throws Exception {
        when(weMakeAGetRequestTo("http://localhost:8080/starWarsCharacter"));
        then(theStatusCode(), is(200));
        then(theBody(), is("{\"Description\": \"Luke Skywalker is a Human from Tatooine\"}"));
    }

}
public class HelloWorldTest extends AbstractAcceptanceTest {

    @Test
    public void shouldReturnHelloWorld() throws Exception {
        when(weMakeAGetRequestTo("http://localhost:8080/hello"));
        then(theStatusCode(), is(200));
        then(theBody(), is("Hello from a servlet!!!!"));
    }

    @Test
    public void shouldFail() throws Exception {
        when(weMakeAGetRequestTo("http://localhost:8080/a/bad/url"));
        then(theStatusCode(), is(404));
    }

}

Notice the difference in the then() lines. Now we are using the Yatspec then(stateStractor, matcher) method, which is the one we were supposed to be using from the beginning. It slipped through me the first time I wrote the tests, because I was completely focused on making the green. When trying to extract the Whens and Thens I had problems because I couldn’t move the fields, so I realized I had a problem. This is an example of how refactoring forced me to use a good design aspect that I wasn’t looking for.

I also extracted the HelloWorldHttpServlet from ExampleApp. Regarding StarWarsCharacterHttpServlet, a couple of method extractions made the class more readable. It is still not perfect and it will require more refactoring as the functionality on the application grows, but it seems clear enough for now.

public class StarWarsCharacterHttpServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        CloseableHttpResponse lukeResponse = getRequestTo("http://swapi.co/api/people/1/");
        DocumentContext lukeJsonDocument = JsonPath.parse(EntityUtils.toString(lukeResponse.getEntity()));

        String lukeName = lukeJsonDocument.read("$.name");

        String lukeSpecies = extractName(getRequestTo(lukeJsonDocument.read("$.species[0]")));
        String lukeHomeworld = extractName(getRequestTo(lukeJsonDocument.read("$.homeworld")));

        response.getWriter().print(format("{\"Description\": \"%s is a %s from %s\"}", lukeName, lukeSpecies, lukeHomeworld));
        response.setStatus(200);
    }

    private String extractName(CloseableHttpResponse lukeSpeciesResponse) throws IOException {
        return JsonPath.parse(EntityUtils.toString(lukeSpeciesResponse.getEntity())).read("$.name");
    }

    private CloseableHttpResponse getRequestTo(String uri) throws IOException {
        HttpGet lukeRequest = new HttpGet(uri);
        return HttpClientBuilder.create().build().execute(lukeRequest);
    }
}

Repo: https://github.com/obprado/EmbeddedJetty/tree/6399b499d1275bedc5eb7c02252ae58205e45f29

Advertisements

One Comment Add yours

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 )

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s