Pages

Test driven development with SpringBoot

Test driven development is a buzz-word nowadays in the software development community. However many developers do not do a very good job in this area. That's a separate topic. In this article, I will explain how we can test different components of a Spring Application.

There are basically three different components in a Spring Application.
  1. Controller Layer
  2. Client Layer
  3. Data Layer
The controller layer is the one which receives the requests from the client.
The client layer is responsible to call external services from our application.
The data layer is responsible to connect to various databases and get/persist data for us.

Actually there can be other layers as well such as business layer where some validations might be done, but writing unit tests for those components is pretty straight forward. Just test each method, mock any external dependencies with Mockito, or Powermock etc and cover the scenarios.

When you create a SpringBoot application, by default spring creates a test class with following annotations

@RunWith(SpringRunner.class)
@SpringBootTest

Normally, developers used the @SpringBootTest annotation to test all the components. But this annotation initializes the whole application context and if there are large number of test classes, using this annotation could be time consuming as you are configuring the application context multiple and this is an expensive operation.

Spring provides a number of annotations to test different layers of the application. When you use the layer specific test, the whole context is not created and the tests run pretty faster.

Annotations provided by Spring:

  • @SpringBootTest
  • @WebMvcTest
  • @RestClientTest
  • @DataJpaTest
  • @JsonTest
  • @DataMongoTest 
One thing to keep in mind is that, when you use @WebMvcTest, the regular @component elements are not scanned. The webmvctest annotation is normally used to test one controller endpoint, and its frequently used with @MockMvc which offers a powerful way to test mvc elements without a full HTTP server.

Sample code:

@RunWith(SpringRunner.class)
@WebMvcTest(MyController.class)
@ActiveProfiles("unittest")
@Category(UnitTest.class)
public class MySampleTest {

 @Autowired
 private MockMvc mockMvc;
 
 @MockBean
 private MyAsyncService mySvc;
 
 @MockBean
 private MyServiceClient myClient;
 
 @MockBean
 private RestTemplate restTemplate;
 
 @Test
 public void test_pageNotFound() throws Exception {
  MockHttpServletResponse response = 
    this.mockMvc.perform(post("/my-endpoint/1234"))
    .andReturn()
    .getResponse();
  assertThat(response.getStatus()).isNotEqualTo(404);
 }
...

The next annotation is @RestClientTest, which can be used if you want to test the REST clients. By default, if configures Jackson and Gson support, configure a RestTemplateBuilder and add support for MockRestServiceServer (intercepts the http call). 

Sample code:

@RunWith(SpringRunner.class)
@RestClientTest(RemoteVehicleDetailsService.class)
public class ExampleRestClientTest {

    @Autowired
    private RemoteVehicleDetailsService service;

    @Autowired
    private MockRestServiceServer server;

    @Test
    public void getVehicleDetailsWhenResultIsSuccessShouldReturnDetails()
            throws Exception {
        this.server.expect(requestTo("/greet/details"))
                .andRespond(withSuccess("hello", MediaType.TEXT_PLAIN));
        String greeting = this.service.callRestService();
        assertThat(greeting).isEqualTo("hello");
    }

}

DataJpaTest annotation is used to test JPA applications. By default it configures in-memory embedded database, scan for @Entity classes and configure SpringDataJPA repositories. The Data JPA tests have transactional and rollback at the end of each test by default.

Sample code:

@RunWith(SpringRunner.class)
@DataJpaTest
@Transactional( propagation=Propagation.NOT_SUPPORTED)
public class ExampleRepositoryTests {

    @Autowired
    private TestEntityManager entityManager;

    @Autowired
    private UserRepository repository;

    @Test
    public void testExample() throws Exception {
        this.entityManager.persist(new User("sboot", "1234"));
        User user = this.repository.findByUsername("sboot");
        assertThat(user.getUsername()).isEqualTo("sboot");
        assertThat(user.getVin()).isEqualTo("1234");
    }

}

Also there are some other best practices that you might wanna use.

When you need the whole application context to be initialized, you can configure spring to use a random port.

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)


The random port can be accessed inside your code by following.

@LocalServerPort
private int port;

Enjoy Test Driven Development !!!


References:
https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-testing.html