Chapter 4. Microservices testing

This chapter covers

  • What types of testing do you need to consider?
  • Which tools are appropriate for microservices?
  • Implementing unit testing for microservices
  • Implementing integration testing for microservices
  • Using consumer-driven contract testing

Where to start! So many types and levels of testing can be implemented for anything. Complicating things further is that different people will likely have different points of view, specifically in regard to what the various types of testing should accomplish.

Let’s get on the same page with respect to the types of testing and create a common understanding of their meaning for us all! In this chapter, you’ll focus only on the types of testing that are relevant for our purposes. There are too many types of testing to cover them all; it’d become overwhelming.

Then you’ll use the admin service you created in chapter 2 to show the types of testing that can be performed with a microservice.

4.1. What type of testing do you need?

Three types of testing are covered in this chapter:

  • Unit testing is focused on testing the internals of your microservice.
  • Integration testing covers the entirety of your service, in addition to the way it interacts with external services, such as a database.
  • Consumer-driven contract testing deals with the boundary between a consumer of your microservice and the microservice itself, via a Pact document that defines the contract.

It’s important to note that unit and integration testing are far from new concepts. They’ve been part of software development for decades. The application of integration testing to microservices may increase its complexity, through more external integration points, but the way we develop them hasn’t greatly changed.

Why did I choose these three types of testing to focus on, given that dozens of types are available? I’m not saying that these three are the only types you need to worry about, but these are certainly crucial to your goal of ensuring that a microservice is as robust as possible. Unit and integration testing are focused on ensuring that what you, as a developer of a microservice, have written meets the requirements that have been outlined for a microservice. Consumer-driven contract testing changes perspectives to look from outside a microservice, to ensure that a microservice can correctly process whatever clients are passing to you. Though it may not be part of the requirements of a microservice, it’s possible that a client expects slightly different behavior than has been developed. Figure 4.1 shows how the three types of testing fit in terms of your code.

Figure 4.1. Types of testing

The key point with respect to testing of any type is that you’re not writing tests for fun or for one-off execution. The purpose, and benefit, of writing any test is the ability to continually execute it against code as it changes and is modified, typically as part of a continuous integration process that regularly builds your code. And why do you want these tests running all the time on old and updated code? For the simple reason that it reduces the number of errors, or bugs, that make it into production code. As I mentioned in chapter 1, anything you can do to reduce the number of times you’re called about production bugs, the better you are for it.

4.2. Unit testing

Typically created by developers as part of writing code, unit testing tests the internal behavior of classes and their methods. Doing so often requires mocks or stubs to mimic the behavior of external systems.

Note

Stubs and mocks are tools you can employ to make it possible to unit test code that interacts with external services, such as a database, without needing a database. Though serving the same purpose, they operate in different ways. Stubs are handcrafted implementations of a service that a developer has written to return precanned responses to each method. Mocks offer greater flexibility, as each test can set up whatever you expect the method to return for that particular test, and then verify that the mock acted in the way you anticipated. Testing with mocks requires each test to set the expectations for the service being called and then verify it afterward, but it saves you from writing every test situation possible into a stub.

Why do you need unit testing at all? You need to ensure that a method on a class performs the function as it’s intended. If a method has parameters passed to it, these should be validated to ensure they’re appropriate. This can be as simple as ensuring that the value is non-null, or as complex as validating an email address format. Likewise, you need to verify that passing particular inputs as parameters returns the result you expect from those inputs. Unit testing is the lowest level of testing, but is often the most crucial to get right. If your smallest unit of code, a method, doesn’t perform as you expect, then your entire service could function incorrectly.

The two most popular and widely used frameworks for this level of testing are JUnit (http://junit.org/) and TestNG (http://testng.org/doc/). JUnit has been around the longest and was the inspiration for TestNG being created. There aren’t many differences between their features, or even the names of annotations in some cases!

The biggest difference is in their goals. JUnit’s focus is purely on unit testing, and was a huge driver for the adoption of test-driven development. TestNG aims to support wider testing use cases than just unit testing.

Whichever a developer chooses is purely a personal choice. At any time, JUnit or TestNG may have more features than the other, but it’s highly likely that the other will soon catch up. Such back and forth has happened over the years many times.

The code as it stood from chapter 2 has been copied to /chapter4/admin, to enable you to see the differences in the code after you’ve added tests. This is particularly important to show relevant code changes that were required to fix any dreaded bugs found. For writing our unit tests, I use JUnit, simply because I’ve used that framework the most in my career and I’m the most familiar with it.

The admin microservice is focused on CRUD operations for the Category model at the moment, and has a JAX-RS resource for providing the RESTful endpoints to interact with it.

As you’re dealing with unit testing, Category is the only viable code you can test with unit tests without mocking databases. It’s certainly possible to mock out EntityManager to test the JAX-RS resource as well, but it’s preferable to test it fully with a database as part of integration testing.

The first thing you need to do is add dependencies to your pom.xml for testing:

<dependency>
  <groupId>junit</groupId>
  <artifactId>junit</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.easytesting</groupId>
  <artifactId>fest-assert</artifactId>
  <scope>test</scope>
</dependency>

Now let’s take a look at some unit tests for Category, as it’s the lowest level in any method execution stack at runtime.

Listing 4.1. CategoryTest
public class CategoryTest {
  @Test
  public void categoriesAreEqual() throws Exception {                      1
    LocalDateTime now = LocalDateTime.now();
    Category cat1 = createCategory(1, "Top", Boolean.TRUE, now);           2
    Category cat2 = createCategory(1, "Top", Boolean.TRUE, now);

    assertThat(cat1).isEqualTo(cat2);                                      3
    assertThat(cat1.equals(cat2)).isTrue();
    assertThat(cat1.hashCode()).isEqualTo(cat2.hashCode());
  }

  @Test
  public void categoryModification() throws Exception {                    4
    LocalDateTime now = LocalDateTime.now();
    Category cat1 = createCategory(1, "Top", Boolean.TRUE, now);
    Category cat2 = createCategory(1, "Top", Boolean.TRUE, now);

    assertThat(cat1).isEqualTo(cat2);
    assertThat(cat1.equals(cat2)).isTrue();
    assertThat(cat1.hashCode()).isEqualTo(cat2.hashCode());

    cat1.setVisible(Boolean.FALSE);

    assertThat(cat1).isNotEqualTo(cat2);
    assertThat(cat1.equals(cat2)).isFalse();
    assertThat(cat1.hashCode()).isNotEqualTo(cat2.hashCode());
  }

  @Test
  public void categoriesWithIdenticalParentIdAreEqual() throws Exception { 5
    LocalDateTime now = LocalDateTime.now();
    Category parent1 = createParentCategory(1, "Top", now);
    Category parent2 = createParentCategory(1, "Tops", now);
    Category cat1 = createCategory(5, "Top", Boolean.TRUE, now, parent1);
    Category cat2 = createCategory(5, "Top", Boolean.TRUE, now, parent2);

    assertThat(cat1).isEqualTo(cat2);
    assertThat(cat1.equals(cat2)).isTrue();
    assertThat(cat1.hashCode()).isEqualTo(cat2.hashCode());
  }
  private Category createCategory(Integer id, String name, Boolean visible,
        LocalDateTime created, Category parent) {                          6

    return new TestCategoryObject(id, name, null,
        visible, null, parent, created, null, 1);
  }
}

  • 1 Test for verifying that two Category instances are identified as equal in all ways.
  • 2 Uses a helper method on the test to create any Category instances you need for testing
  • 3 Uses fluent methods from Fest Assertions to simplify test code
  • 4 Test for ensuring that a Category is different after calling a setter on it.
  • 5 Test of whether a parent with the same ID on a Category is considered equal.
  • 6 Helper method to create a Category instance for testing.

You may have noticed that in the createCategory() method of the test class, you instantiated a TestCategoryObject class. Where did that come from? TestCategoryObject has an important purpose for our testing. Because it extends Category, you can directly set fields such as id and version that have only getter methods on Category. This allows you to retain the important immutability parts of Category, while still being able to set and change the properties of Category that you need for testing. TestCategoryObject provides two constructors that allow you to set the ID of a Category, which is extremely useful for testing. Take a look at the chapter code (on GitHub or downloaded from www.manning.com/books/enterprise-java-microservices) for the full code listing.

4.3. What is immutability?

Immutability is a concept from object-oriented programming for identifying whether an object’s state can be altered. An object’s state is considered immutable if it can’t be altered after its creation.

In our case, Category isn’t entirely immutable, but id, created, and version are fields that you want to be immutable. For that reason, Category has only getter methods defined for them, no setter methods.

To run the tests with Maven from inside /chapter4/admin, you run this:

mvn test -Dtest=CategoryTest

When running CategoryTest with the existing code from chapter 2, you see a failure! The categoriesWithIdenticalParentIdAreEqual() test fails, because it doesn’t consider the two categories to be equal.

With any test failure, two possibilities exist for what happened. Did you make incorrect assertions in your test, or is there a bug in your code?

In this case, do you expect a Category with the same ID but different names to be equal? A first instinct might be to say no, they shouldn’t be equal. But for this situation, you need to remember that the ID is a unique identifier for Category, so you’d expect there to be only a single Category with any particular ID present. So here it’s apparent that your test assertions are correct, as the name of a Category could’ve been modified in subsequent requests, but there’s a bug in your code in how it determines whether a Category is equal.

Let’s take a look at the equals() implementation you currently have on Category, which was autogenerated by an IDE:

public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Category category = (Category) o;
    return Objects.equals(id, category.id) &&
            Objects.equals(name, category.name) &&
            Objects.equals(header, category.header) &&
            Objects.equals(visible, category.visible) &&
            Objects.equals(imagePath, category.imagePath) &&
            Objects.equals(parent, category.parent) &&
            Objects.equals(created, category.created) &&
            Objects.equals(updated, category.updated) &&
            Objects.equals(version, category.version);
}

You can see that you’re comparing the entirety of the parent of each Category instance. As you saw in your test, a parent Category with the same ID but different names will fail an equality test.

From what we discussed earlier, it doesn’t make sense to compare the entire state of one parent category with another. There’s always a chance that one category instance might be retrieved after another, and in between those retrievals the parent category could be updated with a different name. Although the ID is the same, other state differs between the two instances.

You can resolve this conflict by concerning yourself with only the ID of the parent category, and not the entire object state:

public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Category category = (Category) o;
    return Objects.equals(id, category.id) &&
            Objects.equals(name, category.name) &&
            Objects.equals(header, category.header) &&
            Objects.equals(visible, category.visible) &&
            Objects.equals(imagePath, category.imagePath) &&
            (parent == null ? category.parent == null
                : Objects.equals(parent.getId(), category.parent.getId())) &&
            Objects.equals(created, category.created) &&
            Objects.equals(updated, category.updated) &&
            Objects.equals(version, category.version);
}

Here you’ve modified the parent equality check to verify whether either parent is null, before comparing whether the ID value is equal. This change makes your code more robust and less error prone.

A similar change is required to Category.hashCode() to ensure that you include the parent category ID only when generating a hash for a Category instance.

You’ve just seen how some short unit tests can assist in improving your internal code by reducing the potential for bugs. Let’s take the next step and write some integration tests!

4.4. Integration testing

Integration testing is similar to unit testing, and uses the same frameworks, but it’s also used to test a microservice interaction with external systems. This could include databases, messaging systems, other microservices, or pretty much anything it needs to talk to that isn’t internal code to the microservice. If you had unit tests that used mocks or stubs to integrate with external systems, as part of integration testing the mocks and stubs are replaced with calls to the actual systems instead. Removing mocks or stubs opens your code to execution paths that haven’t been tested before, as well as introducing more test scenarios as you need to test handling of errors in those external systems.

Depending on the type of systems a microservice integrates with, it may not be possible to execute these tests on a local developer’s machine. Integration testing is perfectly suited to continuous improvement environments, where resources are more plentiful and any systems that are required can be installed.

With integration testing, you can expand the scope of what you’re intending to test and verify that it works as you expect. It also allows you to use external systems as part of your testing as opposed to mocking anything external. Testing with the actual services and systems that a microservice will rely on in production greatly improves your confidence that going to production won’t result in errors from your code changes. You aren’t going to be running your integration tests against production systems, but you can run them against systems that closely mirror production setup and data.

To assist in developing integration tests, you’ll be using Arquillian. Arquillian is a highly extensible testing platform for the JVM that allows the easy creation of integration, functional, and acceptance tests. Many extensions to the core of Arquillian exist to handle specific frameworks, such as JSF, or for browser testing integration with Selenium. Full details of all the extensions available for Arquillian can be found at http://arquillian.org/.

I’ve chosen Arquillian to help with integration testing because it assists in replicating a production environment as closely as possible without being in production. Your services are started in the same runtime container as would be the case in production, so your service has access to CDI injection, persistence, or whatever runtime pieces your service needs.

To be able to use Arquillian for integration testing, you need to add the necessary dependencies into your pom.xml:

<dependency>
  <groupId>io.thorntail</groupId>
  <artifactId>arquillian</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.jboss.arquillian.junit</groupId>
  <artifactId>arquillian-junit-container</artifactId>
  <scope>test</scope>
</dependency>

The first dependency adds the runtime container for Thorntail to be used within Arquillian tests, and the second adds the integration you require between Arquillian and JUnit. For Arquillian to be able to deploy anything, it needs access to a runtime container. The arquillian dependency of Thorntail registers itself with Arquillian as being a runtime container, enabling Arquillian to deploy to it. Without either of these, you couldn’t execute your integration tests within a runtime container.

To simplify the code required in your tests to execute HTTP requests, you’ll use REST Assured, which also needs to be added to your pom.xml:

<dependency>
  <groupId>io.rest-assured</groupId>
  <artifactId>rest-assured</artifactId>
  <scope>test</scope>
</dependency>

The focus of your integration testing will be on the JAX-RS Resource class, as it defines the RESTful endpoints a consumer will interact with, as well as persisting changes to the database. With integration testing we focus on the provider side of microservice interactions—you’re only validating that your service API works as you’ve designed. This doesn’t take into account what a consumer expects of your API; that’s dealt with in consumer-driven contract testing.

To begin, as shown in listing 4.2, you’ll create a test to verify that all categories from the database are correctly retrieved. This single integration test will verify that your external-facing API returns information that’s expected, as well as validate that your persistence code is properly reading database entries to return. Either of those aspects not working as you expect will result in the test failing.

Listing 4.2. Retrieve all categories in listing 4.1’s CategoryResourceTest
@RunWith(Arquillian.class)                                  1
@DefaultDeployment                                          2
@RunAsClient                                                3
@FixMethodOrder(MethodSorters.NAME_ASCENDING)               4
public class CategoryResourceTest {

    @Test
    public void aRetrieveAllCategories() throws Exception {
        Response response =                                 5
                when()
                        .get("/admin/category")
                .then()
                        .extract().response();

        String jsonAsString = response.asString();
        List<Map<String, ?>> jsonAsList =
JsonPath.from(jsonAsString).getList("");

        assertThat(jsonAsList.size()).isEqualTo(21);        6

        Map<String, ?> record1 = jsonAsList.get(0);         7

        assertThat(record1.get("id")).isEqualTo(0);
        assertThat(record1.get("parent")).isNull();
        assertThat(record1.get("name")).isEqualTo("Top");
        assertThat(record1.get("visible")).isEqualTo(Boolean.TRUE);
    }
}

  • 1 Use Arquillian runner for JUnit test.
  • 2 Create the deployment for Arquillian based on the type of Thorntail project (WAR or JAR).
  • 3 You’re testing RESTful endpoints of your microservice, so you execute the tests as a client.
  • 4 Run test methods in order based on the name.
  • 5 REST Assured’s fluent methods for executing HTTP requests
  • 6 Verify that you received all categories from the database that you expected.
  • 7 Retrieve a single category record from the list and then verify its values.

The first line in your test is to tell JUnit, via @RunWith, that you want to use an Arquillian test runner. @DefaultDeployment informs the Thorntail integration with Arquillian to create an Arquillian deployment to execute the tests against, which will use the type of Maven project to create a WAR or JAR for deployment.

The other key annotation on the test class is @RunAsClient. This annotation tells Arquillian that you want to treat the deployment as a black box and execute the tests from outside the container. Not including the annotation would indicate to Arquillian that the tests are intended to be executed within the container. It’s also possible to mix the use of @RunAsClient on individual test methods, but in this case you’re testing entirely from outside the container.

The test itself executes an HTTP GET request on "/admin/category" and converts the response JSON into a list of maps with key/value pairs. You verify that the size of the list you get back matches the number of Category records you know are present in the database, and then you retrieve the first map from the list and assert that the details on the Category match the root-level category in the database.

As with the unit test, you execute your integration test with this:

mvn test

As the test executes, you’ll see the Thorntail container starting and the SQL being executed to insert the initial category records into the database, as was discussed in chapter 2. With this first test run, you have one successful test with Category-ResourceTest, in addition to the existing CategoryTest unit tests.

Let’s add a test for retrieving a single category directly, but also map the JSON you receive onto a Category object to verify that deserialization is working. This test differs from the previous one in that it uses a different method on the EntityManager from JPA to retrieve a single Category instead of all of them. There’s the double bonus that you’re testing additional methods on your JAX-RS resource, but also validating that your persistence and database entities are properly defined.

Listing 4.3. Retrieve category in CategoryResourceTest
    @Test
    public void bRetrieveCategory() throws Exception {
        Response response =
                given()
                        .pathParam("categoryId", 1014)                  1
                .when()
                        .get("/admin/category/{categoryId}")            2
                .then()
                        .extract().response();

        String jsonAsString = response.asString();

        Category category = JsonPath.from(jsonAsString).getObject("",
    Category.class);                                                  3

        assertThat(category.getId()).isEqualTo(1014);
        assertThat(category.getParent().getId()).isEqualTo(1011);
        assertThat(category.getName()).isEqualTo("Ford SUVs");
        assertThat(category.isVisible()).isEqualTo(Boolean.TRUE);
    }

  • 1 Set a parameter into the request for categoryId.
  • 2 Specify where in the URL path the categoryId should be added.
  • 3 Convert the JSON, via deserialization, you received into the Category instance.

If you now execute the test again, your new test fails with this error:

com.fasterxml.jackson.databind.JsonMappingException: Unexpected token
(START_OBJECT), expected VALUE_STRING: Expected array or string.

Following that error in the log is the JSON message you received, but at the end it references the piece of data that caused the issue, ejm.chapter4.admin.model.Category ["created"]. From this, you know that the test had an issue deserializing the created field on Category into a LocalDateTime instance.

To resolve the problem, you need to give the JSON serialization library, in this case Jackson, help to convert your LocalDateTime instance into JSON that the library knows how to deserialize. To give Jackson help, you need to register a JAX-RS provider to add configuration to Jackson with the JavaTimeModule. First, though, you need to add a dependency to the pom.xml, making that available:

<dependency>
  <groupId>com.fasterxml.jackson.datatype</groupId>
  <artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

Now let’s look at the provider:

Listing 4.4. ConfigureJacksonProvider
@Provider                                                     1
public class ConfigureJacksonProvider implements
ContextResolver<ObjectMapper> {                             2

    private final ObjectMapper mapper = new ObjectMapper()
            .registerModule(new JavaTimeModule());            3

    @Override
    public ObjectMapper getContext(Class<?> type) {
        return mapper;
    }
}

  • 1 Identify the class as a JAX-RS provider.
  • 2 Specify that this provider is used for resolving ObjectMapper instances
  • 3 Register JavaTimeModule with the Jackson mapper to correctly serialize LocalDateTime.

Rerunning mvn test, you see the test pass. Another bug resolved by a test!

You’ve now covered two cases of retrieving categories from your RESTful endpoints. Let’s see whether your JAX-RS resource can store data as well.

Listing 4.5. Create category in CategoryResourceTest
    @Test
    public void cCreateCategory() throws Exception {
        Category bmwCategory = new Category();
        bmwCategory.setName("BMW");
        bmwCategory.setVisible(Boolean.TRUE);
        bmwCategory.setHeader("header");
        bmwCategory.setImagePath("n/a");
        bmwCategory.setParent(new TestCategoryObject(1009));

        Response response =
                given()
                        .contentType(ContentType.JSON)                    1
                        .body(bmwCategory)                                2
                .when()
                        .post("/admin/category");

        assertThat(response).isNotNull();
        assertThat(response.getStatusCode()).isEqualTo(201);              3
        String locationUrl = response.getHeader("Location");              4
        Integer categoryId = Integer.valueOf(
            locationUrl.substring(locationUrl.lastIndexOf('/') + 1)       5
        );

        response =
                when()
                        .get("/admin/category")
                .then()
                        .extract().response();

        String jsonAsString = response.asString();
        List<Map<String, ?>> jsonAsList =
     JsonPath.from(jsonAsString).getList("");

        assertThat(jsonAsList.size()).isEqualTo(22);                      6

        response =
                given()
                        .pathParam("categoryId", categoryId)              7
                .when()
                        .get("/admin/category/{categoryId}")              8
                .then()
                        .extract().response();

        jsonAsString = response.asString();

        Category category =
            JsonPath.from(jsonAsString).getObject("", Category.class);    9

        assertThat(category.getId()).isEqualTo(categoryId);               10
        ...
    }

  • 1 Indicate you’re sending JSON in the HTTP request.
  • 2 Set the Category instance you created as the body of the request.
  • 3 Verify you received a response of 201, and the category was created.
  • 4 Location will be the URL of the Category that was created for you.
  • 5 Extract the ID of the Category you created from the Location.
  • 6 Assert that the total number of categories retrieved is now 22 and not 21.
  • 7 Set a path parameter to be the category ID you retrieved from Location for a new GET request.
  • 8 Set the path for the request, defining where the parameter for the category ID needs to be replaced.
  • 9 Deserialize the JSON you received into a Category instance.
  • 10 Validate that the ID on the category matches what you extracted from Location.

The preceding test starts by creating a new Category instance and setting appropriate values on it, including setting a parent with id of 1009. Next you submit a POST request to the RESTful endpoint for Category to create a new record. You validate that the response you received was correct and extract the new id for the category. Then you retrieve all the categories and validate that you now have 22 records instead of 21, and finally retrieve the new record and validate that its information is the same as what you submitted when you created it.

Let’s run mvn test again to see whether your code has any bugs! This time, your test fails because it expected to receive an HTTP status code of 201, but you received 500 instead. What went wrong? If you trace back through the terminal output, you can see the microservice experienced an error:

Caused by: org.hibernate.TransientPropertyValueException:
  object references an unsaved transient instance
    - save the transient instance before flushing:
      ejm.chapter4.admin.model.Category.parent ->
     ejm.chapter4.admin.model.Category

You can see it’s not able to save the link to the parent category that you specified. That’s because the instance you provided to POST doesn’t have any data on it that helps the persistence layer understand that this instance is already saved.

To resolve this, you need to have your RESTful method for creation retrieve the persistence object for the parent category before you attempt to save your new one.

Listing 4.6. CategoryResource
    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    @Transactional
    public Response create(Category category) throws Exception {
        if (category.getId() != null) {
            return Response
                    .status(Response.Status.CONFLICT)
                    .entity("Unable to create Category, id was already set.")
                    .build();
        }

        Category parent;
        if ((parent = category.getParent()) != null && parent.getId() != null) {1
            category.setParent(get(parent.getId()));                            2
        }

        try {
            em.persist(category);
        } catch (Exception e) {
            return Response
                    .serverError()
                    .entity(e.getMessage())
                    .build();
        }
        return Response
                .created(new URI("category/" + category.getId().toString()))
                .build();
    }

  • 1 Check that you have a parent category and ID before trying to retrieve it.
  • 2 Get the parent category and set it on the new category instance.

All you’ve done is add into create() the ability to retrieve a valid parent category from the persistence layer, and then set it onto your new category instance. Everything else in the method is as it was from chapter 2.

Rerunning mvn test, you now see all tests pass! Let’s add one more test, to see whether your error handling can properly reject a bad request.

Listing 4.7. Fail to create category in CategoryResourceTest
    @Test
    public void dFailToCreateCategoryFromNullName() throws Exception {
        Category badCategory = new Category();                           1
        badCategory.setVisible(Boolean.TRUE);
        badCategory.setHeader("header");
        badCategory.setImagePath("n/a");
        badCategory.setParent(new TestCategoryObject(1009));

        Response response =
                given()
                        .contentType(ContentType.JSON)
                        .body(badCategory)
                .when()
                        .post("/admin/category");

        assertThat(response).isNotNull();
        assertThat(response.getStatusCode()).isEqualTo(400);             2

        ...

        response =
                when()
                        .get("/admin/category")
                .then()
                        .extract().response();

        String jsonAsString = response.asString();
        List<Map<String, ?>> jsonAsList =
     JsonPath.from(jsonAsString).getList("");

        assertThat(jsonAsList.size()).isEqualTo(22);                     3
    }

  • 1 Create a Category instance with no name set.
  • 2 Should receive HTTP status code 400.
  • 3 Validate you still have only 22 categories in the database.

Running mvn test with this new test method results in a failure. Your test is expecting a response code of 400, but you receive 500 instead.

Scrolling through the terminal output, you see this:

Caused by: javax.validation.ConstraintViolationException:
    Validation failed for classes [ejm.chapter4.admin.model.Category]
        during persist time for groups [javax.validation.groups.Default, ]
List of constraint violations:[
    ConstraintViolationImpl{
        interpolatedMessage='may not be null', propertyPath=name,
        rootBeanClass=class ejm.chapter4.admin.model.Category,
        messageTemplate='{javax.validation.constraints.NotNull.message}'
    }
]

Though that’s the correct error you’d expect to see in the logs, your microservice isn’t handling the error properly. On completion of the RESTful method, the transaction was trying to commit the database changes, but that failed because you didn’t have a valid Category instance.

You need to bring forward the point at which the validation occurs, so that your method can properly handle it and return the response code you desire (listing 4.8).

Listing 4.8. CategoryResource.create()
        try {
            em.persist(category);
            em.flush();                                     1
        } catch (ConstraintViolationException cve) {        2
            return Response                                 3
                    .status(Response.Status.BAD_REQUEST)
                    .entity(cve.getMessage())
                    .build();
        } catch (Exception e) {
            return Response
                    .serverError()
                    .entity(e.getMessage())
                    .build();
        }

  • 1 Flush the changes present in the entity manager.
  • 2 Catch any constraint-specific exceptions.
  • 3 Return the response with 400 status code and error messages.

All you’ve done here is modify create() to flush the changes in the entity manager, which causes the validation to be triggered, and then catch any constraint violations to return a response. Running mvn test with this change now allows the test to pass, because it’s now returning the correct response code.

Integration testing is a crucial piece that all microservices need. As you’ve just seen, it’ll quickly identify potential failure points in integrating with external systems, such as a database, caused by situations that the existing code wasn’t written to handle. Integrating with databases and transferring data via HTTP requests are two common uses for which problems with your existing code can be exposed.

Developers are human; we make mistakes. Proper integration testing is a key way to ensure that you’ve developed code that matches what’s expected. It’s often a good idea to have a different developer create these types of tests, because another developer won’t have any preconceived notions about how the code works and will be concerned only with testing the required functionality of the microservice.

4.5. Consumer-driven contract testing

When developing a microservice, you don’t necessarily have real consumers of your service available to test against. But if a service can be provided with details of what a consumer will pass on a request, and what the expected response is, then you can execute those expectations against your real service to ensure that you meet them. What better way to validate that your service’s API works than a consumer specifying what it’s expecting for you to test with!

Consumer-driven contract testing uses this approach, as you’re testing both a consumer and a provider to ensure that proper information is passed between them. How do you do that? Figure 4.2 shows how to use a mock server to capture requests from a consumer, and return the response that was defined for that request.

Figure 4.2. Mock responses to a client request

Bear in mind, the response being returned is what the developer of the consumer thinks should be returned. This expectation can easily differ from the service’s response, but then again, finding those types of problems is the benefit of this type of testing.

By executing what’s shown in figure 4.2, a contract of what the consumer is expecting to send and receive when communicating with the provider microservice can be created. Figure 4.3 shows how it’s then possible to replay those requests on your service, with the service returning a response based on its actual code. Then each response received from the service can be compared against what’s expected, to ensure that both consumer and provider are in agreement about what should occur.

Figure 4.3. Requests sent to the microservice

A popular tool for testing these concepts is Pact (https://docs.pact.io/), which you’ll use in listing 4.10. The process sounds tricky, but it’s not too bad when using Pact. Pact is a family of frameworks that makes it easy to create and use tests for consumer-driven contracts.

The first thing you need to do is create a consumer that’s trying to integrate with the admin microservice, shown next. In chapter4/admin-client, you have the following consumer.

Listing 4.9. AdminClient
public class AdminClient {
    private String url;

    public AdminClient(String url) {                                           1
        this.url = url;
    }

    public Category getCategory(final Integer categoryId) throws IOException { 2
        URIBuilder uriBuilder;
        try {
            uriBuilder = new URIBuilder(url).setPath("/admin/category/" +
categoryId);
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }

        String jsonResponse =
                Request
                    .Get(uriBuilder.toString())
                    .execute()
                        .returnContent().asString();

        if (jsonResponse.isEmpty()) {
            return null;
        }

        return new ObjectMapper()
                .registerModule(new JavaTimeModule())
                .readValue(jsonResponse, Category.class);                      3
    }
}

  • 1 Constructor for AdminClient that takes the URL representing the admin microservice
  • 2 Method to retrieve a single Category by its ID
  • 3 Use Jackson to map the response JSON into Category, registering the JavaTimeModule as well.

You now have a basic client for interacting with the admin microservice. To have Pact create the necessary contract for it, you need to add it as a dependency in pom.xml:

<dependency>
  <groupId>au.com.dius</groupId>
  <artifactId>pact-jvm-consumer-junit_2.12</artifactId>
  <scope>test</scope>
</dependency>

This dependency specifies that you’ll be using JUnit to generate the contract. Let’s create a JUnit test to generate the contract.

Listing 4.10. ConsumerPactTest
public class ConsumerPactTest extends ConsumerPactTestMk2 {                1
    private Category createCategory(Integer id, String name) {             2
        Category cat = new TestCategoryObject(id,
LocalDateTime.parse("2002-01-01T00:00:00"), 1);
        cat.setName(name);
        cat.setVisible(Boolean.TRUE);
        cat.setHeader("header");
        cat.setImagePath("n/a");

        return cat;
    }

    @Override
    protected RequestResponsePact createPact(PactDslWithProvider builder) {3
        Category top = createCategory(0, "Top");

        Category transport = createCategory(1000, "Transportation");
        transport.setParent(top);

        Category autos = createCategory(1002, "Automobiles");
        autos.setParent(transport);

        Category cars = createCategory(1009, "Cars");
        cars.setParent(autos);

        Category toyotas = createCategory(1015, "Toyota Cars");
        toyotas.setParent(cars);

        ObjectMapper mapper = new ObjectMapper()
                .registerModule(new JavaTimeModule());

        try {
            return builder
                    .uponReceiving("Retrieve a category")
                        .path("/admin/category/1015")
                        .method("GET")
                    .willRespondWith()
                        .status(200)
                        .body(mapper.writeValueAsString(toyotas))
                    .toPact();                                             4
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }

        return null;
    }

    @Override
    protected String providerName() {                                      5
        return "admin_service_provider";
    }

    @Override
    protected String consumerName() {                                      6
        return "admin_client_consumer";
    }

    @Override
    protected PactSpecVersion getSpecificationVersion() {                  7
        return PactSpecVersion.V3;
    }

    @Override
    protected void runTest(MockServer mockServer) throws IOException {     8
        Category cat = new
     AdminClient(mockServer.getUrl()).getCategory(1015);

        assertThat(cat).isNotNull();
        assertThat(cat.getId()).isEqualTo(1015);
        assertThat(cat.getName()).isEqualTo("Toyota Cars");
        assertThat(cat.getHeader()).isEqualTo("header");
        assertThat(cat.getImagePath()).isEqualTo("n/a");
        assertThat(cat.isVisible()).isTrue();
        assertThat(cat.getParent()).isNotNull();
        assertThat(cat.getParent().getId()).isEqualTo(1009);
    }
}

  • 1 Extend ConsumerPactTestMk2 to have the required integration hooks for Pact and JUnit.
  • 2 Helper method for creating categories with the required creation date
  • 3 Return the Pact that the consumer expects.
  • 4 Define what should be received as a response based on the request that’s received.
  • 5 Set a unique name for the provider.
  • 6 Set a unique name for the consumer.
  • 7 Which version of the Pact specification you should use for the contract
  • 8 Run the AdminClient against the Pact mock server and verify the expected results.

Though there’s a lot here, the listing boils down to the following:

  • A method that identifies what should be returned from a request to the admin microservice, given a particular response it receives. This is what Pact uses to mock the provider side of the contract creation process.
  • A method to use your client code that interacts with the mock server from Pact, and verifies that the response object you receive has the appropriate values.

Running mvn test will then execute the JUnit Pact test and produce a JSON file in /chapter4/admin-client/target/pacts.

Listing 4.11. Pact JSON output
{
    "provider": {
        "name": "admin_service_provider"
    },
    "consumer": {
        "name": "admin_client_consumer"
    },
    "interactions": [
        {
            "description": "Retrieve a category",
            "request": {
                "method": "GET",
                "path": "/admin/category/1015"
            },
            "response": {
                "status": 200,
                "body": {
                    "id": 1015,
                    "name": "Toyota Cars",
                    "header": "header",
                    "visible": true,
                    "imagePath": "n/a",
                    "parent": {
                        "id": 1009,
                        "name": "Cars",
                        "header": "header",
                        "visible": true,
                        "imagePath": "n/a",
    ...

    ],
    "metadata": {
        "pact-specification": {
            "version": "3.0.0"
        },
        "pact-jvm": {
            "version": "3.5.8"
        }
    }
}
Note

For brevity, I’ve included only the start of the JSON that’s generated, because all the response data is lengthy.

With this JSON file generated, you can now set up the other side of consumer-driven contract testing: verifying that the provider works as the consumer expects it to.

For simplicity, I’ve manually copied the generated JSON across to /chapter4/admin/src/test/resources/pacts. For serious testing with continuous integration, Pact has other ways to store the JSON so that it can be automatically retrieved when running the provider test.

For verifying the provider, because you require an instance of the admin microservice to be running, you’ll use Maven to execute the Pact verification. The verification will take place in the integration test phase of Maven. First you modify your pom.xml to start and stop the Thorntail container around the integration test phase.

Listing 4.12. Thorntail Maven plugin execution for integration tests
  <plugin>
    <groupId>io.thorntail</groupId>
    <artifactId>thorntail-maven-plugin</artifactId>
    <executions>
      <execution>
        <id>start</id>
        <phase>pre-integration-test</phase>
        <goals>
          <goal>start</goal>                            1
        </goals>
        <configuration>                                 2
          <stdoutFile>target/stdout.log</stdoutFile>
          <stderrFile>target/stderr.log</stderrFile>
        </configuration>
      </execution>
      <execution>
        <id>stop</id>
        <phase>post-integration-test</phase>
        <goals>
          <goal>stop</goal>                             3
        </goals>
      </execution>
    </executions>
  </plugin>

  • 1 Start the microservice during the pre-integration-test phase of Maven.
  • 2 Define locations for the logs of the microservice.
  • 3 Stop the microservice in the post-integration-test phase.

Next you add the Pact plugin to execute the contract against your provider.

Listing 4.13. Pact Maven plugin execution
  <plugin>
    <groupId>au.com.dius</groupId>
    <artifactId>pact-jvm-provider-maven_2.12</artifactId>
    <configuration>
      <serviceProviders>
        <serviceProvider>                                                 1
          <name>admin_service_provider</name>
          <protocol>http</protocol>
          <host>localhost</host>
          <port>8081</port>
          <path>/</path>
          <pactFileDirectory>src/test/resources/pacts</pactFileDirectory> 2
        </serviceProvider>
      </serviceProviders>
    </configuration>
    <executions>
      <execution>
        <id>verify-pacts</id>
        <phase>integration-test</phase>                                   3
        <goals>
          <goal>verify</goal>                                             4
        </goals>
      </execution>
    </executions>
  </plugin>

  • 1 Define the location of the admin microservice provider.
  • 2 Set the directory where the Pact contract files can be found.
  • 3 Pact verification runs during the integration-test phase of Maven.
  • 4 Use the verify goal of the Pact plugin.

Running mvn verify will execute all the tests you’d defined previously, but also run the Pact verification as the last step. You should see output in the terminal to indicate it succeeded:

returns a response which
  has status code 200 (OK)
  has a matching body (OK)

Well, that was smooth, but what does it look like if it doesn’t work? To try that out, you can add the following code into the CategoryResource.get() method, before the current return statement, so that you return a different category for your Pact test:

if (categoryId.equals(1015)) {
    return em.find(Category.class, 1010);
}

If you now run the test with mvn verify again, you’ll see a test failure with output containing the following:

    returns a response which
      has status code 200 (OK)
      has a matching body (FAILED)

Failures:

0) Verifying a pact between admin_client_consumer and admin_service_provider
     - Retrieve a category returns a response which has a matching body
      $.parent.parent.parent.parent -> Type mismatch: Expected Map Map(parent
     -> null, name -> Top, visible -> true, imagePath -> n/a, version -> 1,
     id -> 0, updated -> null, header -> header, created -> List(2002, 1, 1,
     0, 0)) but received Null null

        Diff:

        -{
        -    "parent": null,
        -    "name": "Top",
        -    "visible": true,
        -    "imagePath": "n/a",
        -    "version": 1,
        -    "id": 0,
        -    "updated": null,
        -    "header": "header",
        -    "created": [
        -        2002,
        -        1,
        -        1,
        -        0,
        -        0
        -    ]
        -}
        +

      $.parent.parent.parent.name -> Expected 'Transportation' but received
     'Top'

      $.parent.parent.parent.id -> Expected 1000 but received 0

      $.parent.parent.name -> Expected 'Automobiles' but received
     'Transportation'

      $.parent.parent.id -> Expected 1002 but received 1000

      $.parent.name -> Expected 'Cars' but received 'Automobiles'

      $.parent.id -> Expected 1009 but received 1002

      $.name -> Expected 'Toyota Cars' but received 'Trucks'

      $.id -> Expected 1015 but received 1010

This log message provides detailed information about what crucial data, such as ID and name, it found for each category in the hierarchy, and how that differs from what the Pact contract had defined.

As mentioned previously, such a discrepancy could be a result of an invalid assumption on the part of the consumer, or a bug in the provider. What such a failure really indicates is that developers from the consumer and provider sides need to discuss how the API needs to operate.

4.6. Additional reading

As I mentioned earlier, there are many other types of testing I won’t be covering. A couple of the critical ones are user acceptance testing and end-to-end testing. Though they are both crucial to ensuring that adequate testing is performed, they’re beyond the scope of this book because they deal with a higher level of testing. For additional information on testing with microservices, I recommend Testing Java Microservices by Alex Soto Bueno, Jason Porter, and Andy Gumbrecht (Manning, 2018).

4.7. Additional exercises

Here are some additional tests that you could write to experiment with the different testing methods, and also help improve the code for the example!

  • Add a method to CategoryResourceTest that verifies the ability to update Category.
  • Add a method to CategoryResourceTest to verify that Category can be removed from the database successfully.
  • Add methods to AdminClient for retrieving all categories, adding a category, updating a category, and removing a category. Then add the request/response pairs in ConsumerPactTest.createPact() for the new methods, and update ConsumerPactTest.runTest() to execute and verify each of them.

If you take on any of these exercises and would like to see them included in the code for the book, please submit a pull request to the project on GitHub.

Summary

  • Unit testing is important, but the need to test doesn’t end there. You need to test all aspects of a service as realistically as possible.
  • Arquillian is a great framework for simplifying more-complex testing that requires a runtime container to interact with and provide near-production execution.
  • The key to microservice testing is ensuring that the contract that a microservice defines, the API it exposes, is tested against not only what the microservice intends to expose, but also what a client is expecting to pass and receive.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.142.56