DEV Community

Cover image for Create fast integration tests
Alex Rabelo Ferreira
Alex Rabelo Ferreira

Posted on • Edited on

Create fast integration tests

The developers need to create tests to achieve better quality for systems. There are some challenges to creating tests. The developers need to know the libraries and frameworks for testing. The company needs to create an environment like production. Using Testcontainers, you can create an environment similar to production. It increases the difficulty of creating tests and can increase the pipeline time. One solution for that is to create tests only for system slices and not for the entire system. This article shows how Spring helps us to create these types of tests.

Spring Tests

Tests are one part of development that is difficult for a lot of developers. The developers need to know the library and frameworks to run the tests. The libraries help to integrate the language and the framework. The framework web mocks the dependencies for each infrastructure. So the developers tend to skip the test phase due to these challenges. Spring-Boot came to help with libraries, such as the spring-boot-tests library. The library helps developers create simple tests faster.

Slices tests on Spring Boot

The first Spring-Boot versions only help to create integration tests. These tests set up all the configurations of the system with Spring and waste a lot of time on pipelines. Also, it wastes a lot of computer resources to run. So, Spring-Boot 2.4 added Slices tests in the testing library. This type of test configures specific layers of an application, like the web layer (MVC layer).

It does not configure the database and other infrastructures. So, the test uses fewer resources and runs faster than conventional Spring tests. Also, since the first version, the community has added new Slice Tests. The last added was for Cassandra databases.

Slice tests Types

There are a lot of slice tests on the Spring Boot Test library. The list of types is in the official document. The main types are:

  • MockMVC tests for Web applications
  • JPATest for repository
  • AMQPTest for listeners/senders

MockMVC Tests The Mock

McvTest will start in the Spring only for the web application layer. It won't configure the database or other beans to test this layer. An example of a controller test is below:

@ContextConfiguration(classes = {SpringApplicationLight.class})
@Import({JacksonAutoConfiguration.class, LoggerBeanFactory.class, ResourceExceptionHandler.class})
@WebMvcTest(AuthorizationResource.class)
class AuthorizationResourceIT {

    private final CreateAuthorizationRequest request = RequestFixtures.createAuthorizationRequest();

    @Autowired MockMvc mockMvc;
    @Autowired ObjectMapper objectMapper;

    @MockBean CreateAuthorization useCase;

    private final String baseUrl = "/authorizations";

    @Test
    void shouldReturn200_whenRequestIsValid() throws Exception {
        String authorizationId = "authorizationId";
        ArgumentCaptor<CreateAuthorizationInput> captor = ArgumentCaptor.forClass(CreateAuthorizationInput.class);
        Mockito.when(useCase.execute(captor.capture())).thenReturn(authorizationId);

        mockMvc.perform(post(baseUrl).contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request))).andDo(MockMvcResultHandlers.print())
                .andExpect(status().is2xxSuccessful())
                .andExpect(jsonPath("$.authorizationId").value(authorizationId))
        ;

    }

Enter fullscreen mode Exit fullscreen mode

This code uses mockMvc to create a server with the controller. It creates an object of your controller configured by the @WebMvcTest annotation. After Junit initialization, Spring creates an object to inject into the field mockMvc.

The object will create an HTTP server that receives a request and calls the controller. So, in the test method, MockMvc handles the request to the controller object.

The controller will process the request and return a response object. After, MockMvc will return to us the response to make assertions. We can use MockMvc to assert the status, the body content, and others with expected values.

JPA Tests

Repository tests require a database infrastructure. There is more than one repository per system. We need to create a base class shared between all the repository tests. One example is:

@ActiveProfiles("db")
@DataMongoTest(excludeAutoConfiguration = {
        EmbeddedMongoAutoConfiguration.class,
})
@AutoConfigureDataJpa
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class BaseRepositoryIT {

    private static final DynamicPropertyConfigurableContainer noSqlContainer = new CustomNoSqlContainer();
    private static final DynamicPropertyConfigurableContainer sqlContainer = new CustomDataBaseContainer();

    @DynamicPropertySource
    static void datasourceProperties(DynamicPropertyRegistry dynamicPropertyRegistry) {
        noSqlContainer.configure(dynamicPropertyRegistry);
        sqlContainer.configure(dynamicPropertyRegistry);
    }

}

Enter fullscreen mode Exit fullscreen mode

This is a JPA-based class. We need this base class to create the containers to serve as databases. This is a way to create a lot of repository tests with the same setup.

The class is using two libraries, one for NoSQL and the other for SQL. The DataMongoTest annotation sets up the NoSQL beans. You can use embedded Mongo. We are using the Testcontainers library to create containers for us. So, we exclude the embedded configuration.

We did the same for SQL. The annotation AutoConfigureTestDatabase removes the embedded database. After that, you can set up the properties from each container to Spring properties. Use the DynamicPropertySource annotation to set properties for each database. The Spring needs the port from the container to connect to the databases. We used the dynamic properties to set the ports.

Now, we can create all the repository tests extending from this base class. An example of tests for the repository can be:


class AuthorizationRepositoryIT extends BaseRepositoryIT {

    @Autowired AuthorizationRepository repository;

    private final Authorization authorization = DomainFixtures.createAuthorization();

    @Test

    void shouldSaveAuthorization() {
        Authorization savedAuth = repository.save(authorization);
        assertNotNull(repository.findById(savedAuth.id).orElseThrow());
    }

}

Enter fullscreen mode Exit fullscreen mode

This test uses the repository to save an entity. The database created by TestContainers is the database container. The base configuration doesn't create any other bean. The controllers, HTTP clients, and other types are not created.

Rest client test

You can test REST requests too. One way is to use the Wiremock library. It creates a web server to mock some api. The component tests will call the client REST. Wiremock will mock the request and response of the client. You can generate any response to each scenario that you want to test. The Wiremock can be set up on a container or started a server manually. You only need to set the Spring properties for the API port with the Wiremock server port.

The API client tests are like repository tests. It is common to have more than one client per system. All these tests can share the same configurations. So, we can create a base class to share these configurations among all the client tests.

You can configure Wiremock using a server object or a container. The first option permits you to make more flexible requests and response mocks. The container option helps you to prepare the local test infrastructure. This option keeps the mapping files updated. One change to the API contract will break the component tests. So, in this example, we are using a container to set up Wiremock. We need to create all the files to simulate different scenarios for the tests.

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {
        FeignAutoConfiguration.class,
        FiegnConfiguration.class,
})
@EnableFeignClients
public class BaseContainerClientTest {

    private static final DynamicPropertyConfigurableContainer WIREMOCK_CONTAINER = new CustomWiremockContainer();

    @DynamicPropertySource
    static void datasourceProperties(DynamicPropertyRegistry dynamicPropertyRegistry) {
        WIREMOCK_CONTAINER.configure(dynamicPropertyRegistry);
    }
}
Enter fullscreen mode Exit fullscreen mode

The annotations used above the Base class name are to set up the dependencies common to all clients. You can reuse a container using a static field to create the container. The Testcontainers will keep the container alive until the JVM shuts down. The method implemented set up the properties related to the container. The dynamic properties object will set up the properties for your APIs.

Once you have a base class, you can create a test class for each client. The class needs to extend the base class. The methods use a similar logic. It calls the client method that will make an HTTP request to the WireMock server. The WireMock will return a response. Then the method can assert on the response object.

class GitHubClientTest extends BaseContainerClientTest {

    @Autowired 
     private ClientExample client;

    @Test
   voidshouldThrowErrorWhenClientReturn500() {
       FeignException feignException = Assertions.assertThrows(FeignException.class, () -> client.getRepo("id-to-500"));
       Assertions.assertEquals(500, feignException.status());
    }

    @Test
   voidshouldThrowErrorWhenClientReturn400() {
       FeignException feignException = Assertions.assertThrows(FeignException.class, () -> client.getRepo("id-to-400"));
       Assertions.assertEquals(400, feignException.status());
    }

    @Test
   voidshouldReturnOkWhenClientReturnResponse() {
       FeignException feignException = Assertions.assertThrows(FeignException.class, () -> client.getRepo("id-to-200"));
       Assertions.assertEquals(200, feignException.status());
    }
}
Enter fullscreen mode Exit fullscreen mode

There is a disadvantage to using containers instead of the WireMock server bean. You need to create a response mock in an external directory. It needs to be accessible by the container at runtime. So, you need to re-run the test after updating the mock file. Also, we cannot create two different behaviors for a get method without parameters or headers. In cases where there are many GET methods, use the Wiremock bean instead of the container.

The advantage of using static files is in the development environment. The developers can run the application locally using a Docker Compose file. The tests confirm all the mocks. So, if the developer needs to simulate some scenario for a PRD bug, they can set some values in the mock files. Also, they need to run the Wiremock container to debug the PRD scenario.

But there are some scenarios that the file mapping don't cover. You can use Wiremock server bean to solve this problem. Wiremock bean is an object that the developer can change the mock stubs programatically. The developer can reuse the stubs created for container base class. The base class for this type of test will be as below:

  @ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {
       FeignAutoConfiguration.class,
        FiegnConfiguration.class,
})
@EnableFeignClients
public classBaseServerClientTest {

    private static finalWireMockConfiguration config= WireMockConfiguration.wireMockConfig()
          .usingFilesUnderDirectory("docker");
    protected staticWireMockServer wiremockServer= newWireMockServer(config);

     @DynamicPropertySource
   static voiddatasourceProperties(DynamicPropertyRegistry dynamicPropertyRegistry) {
       wiremockServer.start();
        dynamicPropertyRegistry.add("wiremock-host", wiremockServer::baseUrl);
    }
} 

Enter fullscreen mode Exit fullscreen mode

The class is similar to other base class in the Spring annotations. The main difference is on Wiremock server field that use a config object. You need to use this config to set the directory of yours stubs mapping files to reuse of them. Finally, you need to set the environment variable with the base url from Wiremock server.

The test is the same when using file to stub. The difference now is how to stub. You need to use wiremock server variable to create stub. You have more power to create stub. You can change the return without parameters, you can simulate to return any HTTP response.

classGitHubClientTest extendsBaseServerClientTest {

     @Autowired
   privateGitHubClient client;

    @Test
   voidshouldThrowErrorWhenClientReturn500() {
       FeignException feignException = Assertions.assertThrows(FeignException.class, () -> client.getRepo("id-to-500"));
       Assertions.assertEquals(500, feignException.status());
    }

    @Test
    voidshouldThrowErrorWhenClientReturn400() {
        FeignException feignException = Assertions.assertThrows(FeignException.class, () -> client.getRepo("id-to-400"));
       Assertions.assertEquals(400, feignException.status());
    }

    @Test
    voidshouldReturnOkWhenClientReturnResponse() {
        GitResponse response = client.getRepo("id-to-200");
        Assertions.assertEquals("repo-test", response.repo);
    }

    @Test
    voidshouldReturnOkWhenClientReturnAllResponse() throwsJSONException {
       JSONObject jsonObject = newJSONObject();
       jsonObject.put("repo", "test");
       wiremockServer.stubFor(
              WireMock.get(WireMock.urlEqualTo("/git"))
                    .willReturn(WireMock.okJson(jsonObject.toString()))
        );

       GitResponse response = client.getRepo();
        Assertions.assertEquals("test", response.repo);
    }
}
    class GitHubClientTest extends BaseServerClientTest {

    @Autowired
    private GitHubClient client;

    @Test
    void shouldThrowErrorWhenClientReturn500() {
        FeignException feignException = Assertions.assertThrows(FeignException.class, () -> client.getRepo("id-to-500"));
        Assertions.assertEquals(500, feignException.status());
    }

    @Test
    void shouldThrowErrorWhenClientReturn400() {
        FeignException feignException = Assertions.assertThrows(FeignException.class, () -> client.getRepo("id-to-400"));
        Assertions.assertEquals(400, feignException.status());
    }

    @Test
    void shouldReturnOkWhenClientReturnResponse() {
        GitResponse response = client.getRepo("id-to-200");
        Assertions.assertEquals("repo-test", response.repo);
    }

    @Test
    void shouldReturnOkWhenClientReturnAllResponse() throws JSONException {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("repo", "test");
        wiremockServer.stubFor(
                WireMock.get(WireMock.urlEqualTo("/git"))
                        .willReturn(WireMock.okJson(jsonObject.toString()))
        );

        GitResponse response = client.getRepo();
        Assertions.assertEquals("test", response.repo);
    }
}
Enter fullscreen mode Exit fullscreen mode

Using bean, you can create stubs for different scenarios; you have more power. However, the local environment can not use these stubs. So, prefer to use success scenarios with file stubs.

Conclusion

In this article, we showed how to create integration tests with fewer resources. We showed the power of tests by layers with the Spring Boot framework. You can talk more about tests, call me. You have a special chance to book on my calendly this week. It will be a pleasure to help you solve your problem.

Top comments (0)