The Test Pyramid In Practice (3/5)

le 27/09/2018 par Lyman GILLISPIE
Tags: Software Engineering

The previous article detailed the base of the pyramid: unit tests and their role in preventing regressions in our application. But they’re far from being sufficient, and we need to use other types of tests. In this article, we’ll cover component tests.

This article originally appeared on our French Language Blog on 27/06/2018.

body .gist .highlight {<br /> background: #202020;<br />}<br />body .gist tr:nth-child(2n+1) {<br /> background: #202020;<br />}<br />body .gist tr:nth-child(2n) {<br /> background: #202020;<br />}<br />body .gist .gist-meta {<br /> display:none;<br />}<br />body .gist .blob-num,<br />body .gist .blob-code-inner,<br />body .gist .pl-s2,<br />body .gist .pl-stj {<br /> color: #f8f8f2;<br />}<br />body .gist .pl-c1 {<br /> color: #ae81ff;<br />}<br />body .gist .pl-enti {<br /> color: #a6e22e;<br /> font-weight: 700;<br />}<br />body .gist .pl-st {<br /> color: #66d9ef;<br />}<br />body .gist .pl-mdr {<br /> color: #66d9ef;<br /> font-weight: 400;<br />}<br />body .gist .pl-ms1 {<br /> background: #fd971f;<br />}<br />body .gist .pl-c,<br />body .gist .pl-c span,<br />body .gist .pl-pdc {<br /> color: #75715e;<br /> font-style: italic;<br />}<br />body .gist .pl-cce,<br />body .gist .pl-cn,<br />body .gist .pl-coc,<br />body .gist .pl-enc,<br />body .gist .pl-ens,<br />body .gist .pl-kos,<br />body .gist .pl-kou,<br />body .gist .pl-mh .pl-pdh,<br />body .gist .pl-mp,<br />body .gist .pl-mp1 .pl-sf,<br />body .gist .pl-mq,<br />body .gist .pl-pde,<br />body .gist .pl-pse,<br />body .gist .pl-pse .pl-s2,<br />body .gist .pl-mp .pl-s3,<br />body .gist .pl-smi,<br />body .gist .pl-stp,<br />body .gist .pl-sv,<br />body .gist .pl-v,<br />body .gist .pl-vi,<br />body .gist .pl-vpf,<br />body .gist .pl-mri,<br />body .gist .pl-va,<br />body .gist .pl-vpu {<br /> color: #66d9ef;<br />}<br />body .gist .pl-cos,<br />body .gist .pl-ml,<br />body .gist .pl-pds,<br />body .gist .pl-s,<br />body .gist .pl-s1,<br />body .gist .pl-sol {<br /> color: #e6db74;<br />}<br />body .gist .pl-e,<br />body .gist .pl-ef,<br />body .gist .pl-en,<br />body .gist .pl-enf,<br />body .gist .pl-enm,<br />body .gist .pl-entc,<br />body .gist .pl-entm,<br />body .gist .pl-eoac,<br />body .gist .pl-eoac .pl-pde,<br />body .gist .pl-eoi,<br />body .gist .pl-mai .pl-sf,<br />body .gist .pl-mm,<br />body .gist .pl-pdv,<br />body .gist .pl-som,<br />body .gist .pl-sr,<br />body .gist .pl-vo {<br /> color: #a6e22e;<br />}<br />body .gist .pl-ent,<br />body .gist .pl-eoa,<br />body .gist .pl-eoai,<br />body .gist .pl-eoai .pl-pde,<br />body .gist .pl-k,<br />body .gist .pl-ko,<br />body .gist .pl-kolp,<br />body .gist .pl-mc,<br />body .gist .pl-mr,<br />body .gist .pl-ms,<br />body .gist .pl-s3,<br />body .gist .pl-smc,<br />body .gist .pl-smp,<br />body .gist .pl-sok,<br />body .gist .pl-sra,<br />body .gist .pl-src,<br />body .gist .pl-sre {<br /> color: #f92672;<br />}<br />body .gist .pl-mb,<br />body .gist .pl-pdb {<br /> color: #e6db74;<br /> font-weight: 700;<br />}<br />body .gist .pl-mi,<br />body .gist .pl-pdi {<br /> color: #f92672;<br /> font-style: italic;<br />}<br />body .gist .pl-pdc1,<br />body .gist .pl-scp {<br /> color: #ae81ff;<br />}<br />body .gist .pl-sc,<br />body .gist .pl-sf,<br />body .gist .pl-mo,<br />body .gist .pl-entl {<br /> color: #fd971f;<br />}<br />body .gist .pl-mi1,<br />body .gist .pl-mdht {<br /> color: #a6e22e;<br /> background: rgba(0, 64, 0, .5);<br />}<br />body .gist .pl-md,<br />body .gist .pl-mdhf {<br /> color: #f92672;<br /> background: rgba(64, 0, 0, .5);<br />}<br />body .gist .pl-mdh,<br />body .gist .pl-mdi {<br /> color: #a6e22e;<br /> font-weight: 400;<br />}<br />body .gist .pl-ib,<br />body .gist .pl-id,<br />body .gist .pl-ii,<br />body .gist .pl-iu {<br /> background: #a6e22e;<br /> color: #272822;<br />}<br />body .gist .gist-file,<br />body .gist .gist-data {<br /> border: 0px;<br /> border-bottom: 0px;<br />}<br />

Component tests

Traveling up the pyramid, where we have more integration and less insulation, we reach component tests. Their purpose is to validate our component and its boundary, like integration tests, but to avoid integrating with external dependencies that may be offline or nonfunctional. Component tests live at the crossroads of unit tests -- because of their isolation -- and integration tests -- because they focus on intra / inter component interaction.

Tests de composant

What to test?

This type of test makes it possible to validate the Spring configuration (all the annotations, the application.properties/yml file, the injection of dependencies, etc.) and to ensure that all of the objects in our component integrate correctly. This also includes the interfaces where the component interacts with the outside world. Typically we’re looking to answer questions like:

  • Does the Controller correctly expose our endpoints?
  • Is our database configuration correct? We excluded the Repository from our unit tests, does it work?
  • We also excluded the Client, is it annotated?

Implementation

Spring Controller

While it’s true that we unit-tested the controller individually, we aren’t sure that the Spring configuration is correct. We’ll add tests to verify that the URLs are correct, the Service is well injected, the validation works, and the HTTP errors are managed. We’ll use the Spring MVC Test Framework (MockMvc for those in the know) which allows us to simulate incoming HTTP requests; the framework itself is based on mocks for the Servlet API.

Below an excerpt from the test (Gitlab link):

Voir le lien github

There are some important things to be aware of in this excerpt:

  1. The @WebMvcTest annotation is somewhat lighter-weight than @SpringBootTest: rather than completely auto-configure, it will only configure the MVC part. And, to further reduce scope, we specify the controller we want to load and test. This annotation allows us to use MockMvc, which will:
    1. Simulate HTTP calls to validate our URL mappings,
    2. Validate the input parameters (if you used the validation API)
    3. Check the response that the controller makes (status, body, headers, etc.).
  2. The @AutoConfigureJsonTesters annotation allows us to inject JsonTester utility classes to handle json, and, in our case, to serialize objects. Though an ObjectMapper would have been sufficient.
  3. We use the @MockBean annotation to create a Service mock and replace the same type of Bean in the Spring context; it allows us to be isolated like we were in our unit tests. Although it’s a mock, it still lets us verify that the Service is correctly injected into the Controller by Spring.

These tests will seem very similar to unit tests, but they validate something quite different. In particular, notice that if we compare the two (TU, TC), we can see that the component tests validate the glue around the Controller (i.e. HTTP, deserialization, validation) materialized by the annotations, while the unit tests validate the “business logic".

Database

Regardless of the environment, testing with a real database presents its own set of problems: schema versioning, sample datasets, cleanup, network latency, or unavailability of the database. To mitigate these, we can test using an in-memory database (H2) in lieu of the Postgres database used by the application. This gives us more control over the schema and test data, and, as an added benefit, db access time is also much faster.

Spring Boot provides a mechanism to auto-magically configure an in-memory database: the @DataJpaTest annotation. When associated with the right Maven dependency (H2, HSQL, Derby), Spring will configure a Datasource and an EntityManager, without you having to provide a single line of code or configuration.

Voir le lien github

The objective here isn’t to test the Spring framework, but to test the database insertions. It’s also relevant to test custom methods (i.e findByFromAndTo) and those annotated by @Query.

Note: Spring also provides data access utility classes, typically DataAccessUtils used below.

Note 2: While DataJpaTest is useful for Repository testing, don’t use it for everything. For example, unit test your Services by stubbing the Repositories, there’s no need for Spring or an in-memory database. The same is true for Controller tests. Be aware that Hibernate will be initialized with each test, which will take longer when you have more entities in your model, and keep in mind the value of speedy feedback.

Excerpt from the code (Gitlab link):

Voir le lien github

I can see the sceptics coming from here: "Sure, but H2 isn’t my database, the syntax is different. These tests have no value, they might work with H2 in test and crash in prod." This isn’t completely false. Even if H2 is the generally recommended solution, it’s likely that you use features specific to your RDBMS (for optimization reasons or simply because it does not fully respect SQL standards). If this is the case then H2 won’t meet the need. A somewhat heavier option is to start a Docker container containing your test database during integration tests, this can be done automatically using the Testcontainers library. To do this, you will need the following dependencies:

Voir le lien github

On the code side, and in order to demonstrate the two ways of doing this, we’ll use a profile ("testpg") and an application-testpg.yml file containing the following configuration:

Voir le lien github

At the code level (Gitlab link), we simply activate the profile in question. Apart from the annotations, the rest of the code is identical to the H2 test.

Voir le lien github

When running the test, the framework will use Docker to retrieve a Postgres image and start it. You should see logs similar to this :

2018-06-03 12:44:15.992 INFO 71125 --- [main] ???? [postgres:9.6.8] : Creating container for image: postgres:9.6.8 2018-06-03 12:44:16.153 INFO 71125 --- [main] ???? [postgres:9.6.8] : Starting container with ID: ea2d833e843ef8adc78da779e6b3c62b31e4c1fe8bf851ed82f1bae23f86d0c8 2018-06-03 12:44:16.629 INFO 71125 --- [main] ???? [postgres:9.6.8] : Container postgres:9.6.8 is starting: ea2d833e843ef8adc78da779e6b3c62b31e4c1fe8bf851ed82f1bae23f86d0c8 2018-06-03 12:44:21.361 INFO 71125 --- [main] ???? [postgres:9.6.8] : Container postgres:9.6.8 started

Execution is a little slower - 600 ms once the image is downloaded, versus 400 ms for the test with H2 - but it’s still reasonable if using an in-memory database isn’t possible in your context.

HTTP Client

The problem is similar when testing the HTTP client: we don't want to send requests to the real service, to avoid latency, unavailability, or other unwanted behaviour. To remedy this, we’ll use Wiremock to provide stubs for the HTTP calls.

Voir le lien github

WireMock is configured through a JUnit rule, then used to create a stub for the URL we’re interested in. You can simulate all sorts of things: both the request and the response, and importantly the body of the response (here from a file located in src/main/resources/__files/).

Below the code (Gitlab link):

Voir le lien github

Note: If you only use the @SpringBootTest annotation without parameters, Spring will instantiate the entire context based on the @SpringBootApplication annotated class. It will auto-configure the database (datasource, entityManager, etc.), which, in the case of client testing, will be unacceptably slow. To avoid this, we precisely select the classes that interest us and, in addition, we choose the AutoConfiguration elements useful for the test. This may seem verbose and complicated, even superfluous for some tests, but when you have several tens or hundreds of tests, the time saved is counted in minutes.

Unfortunately, the problem with this type of test is that it will continue to work the day the API changes interface, wiremock doing its doubling job perfectly. To remedy this we can implement integration tests, but there fall on the opposite side: the test will fail as soon as the API is not available. The contract tests allow us to answer this problem, and we’ll see how in the next article.