Keeping going on the series about Spring, last one regarding to spring concepts, here I start the Spring Test, going around the unit tests, integration tests and configuring the application to test.

Introduction

Test is an important part of the development and Spring give to the developers a good support to that activities. The IoC principle used by spring is the key of the benefits. But what to test?

If you are working in a Spring Boot Application you are following a N-Tiers architecture because Spring Boot Applications have three layers (Web, Business and Persistence). The test should be created to test each one of those layers individually and integrated. In the same way, you should test the communication with external service or database.

The unit test target a small unit of code. Any dependencies are removed from it. The integration test target the interaction between the unit parts or different layers.

Unit Test Integration Test
Test one unit of functionality,
Minimal dependencies,
Isolated from the environment,
Uses stubs/mocks for dependencies.
The integration tests will test the interaction of multiple units of work and to test application classes.
- Goals [1]:
  • To manage Spring IoC container caching between tests.
  • To provide Dependency Injection of test fixture instances.
  • To provide transaction management appropriate to integration testing.
  • To supply Spring-specific base classes that assist developers in writing integration tests.
  • When you test a component you have multiple units of test. However, you also can have tests of multiple layers to test the interaction between controller and business, for example, or between the business and repository/API/folders. The same way, you can test the full path, from the controller to repository.


    Good Practices

    The good practices are independent if you use spring or not.

    TDD: The Test Driven Design is a way to start the development by the test: (1) RED - start write a test will fail (2) GREEN - Write code enough to pass (3) REFACTOR - Improve code.[2]

    Builder Pattern: You can use this pattern to improve the test and give support to the reuse of code. A example you can find here: [3]

    BDD: bahavior-driven development. A good test should be readable by humans. It should be clear and follow a natural language. The methodology given-when-then using BDDMockito and AssertJ helps to achieve that goal.[4]

    • given: precondition and requirements for the application
    • when: the action to be tested.
    • then: verification of the result after execute the action
    # Mockito:
    when(methodCall).then(soSomething)
    # BDDMockito:
    given(methodCall).will(doSomething)
    # JUnit:
    assertEquals("MyText", obj.getName());
    # AssertJ:
    assertThat(user.getAge())
      .as("%s should be 100", obj.getName())
      .isEqualTo(100);
    # BDDAssertions:
    then(obj.getAge()).isEqualTo(40);

    Libraries


    Spring Support

    Unit Test

    Packages

    • Environment: "contains mock implementations of the Environment and PropertySource abstractions". When you need test specific properties by environment.
    • JNDI: "contains a partial implementation of the JNDI SPI, which you can use to set up a simple JNDI environment for test suites or stand-alone applications"
    • Servlet API: "contains a comprehensive set of Servlet API mock objects that are useful for testing web contexts, controllers, and filters."
    • Spring Web Reactive: used to mock request and response.

    Classes

    • General Testing Utilities: org.springframework.test.util
    • Spring MVC Testing Utilities: org.springframework.test.web

    Integration Test

    Context Management and Caching

    • The Spring TestContext Framework provides consistent loading of Spring ApplicationContext instances and WebApplicationContext instances as well as caching of those contexts. By default, once loaded, the configured ApplicationContext is reused for each test. Thus, the setup cost is incurred only once per test suite, and subsequent test execution is much faster.[5]

    Dependency Injection of Test Fixtures

    • When the TestContext framework loads your application context, it can optionally configure instances of your test classes by using Dependency Injection. This provides a convenient mechanism for setting up test fixtures by using preconfigured beans from your application context. [6]

    Transaction Management

    • It is a resource to manage the database state. If you need to test using a real database it will run a roll back to avoid change the database. [7]


    Annotations

    Spring has a huge number of annotations to help the development. Here is some of them with the copy of description that you will find in the spring documentation. In the documentation you can see the complete list.

    • Spring Testing Annotations:
      • @ContextConfiguration defines class-level metadata that is used to determine how to load and configure an ApplicationContext for integration tests.
      • @WebAppConfiguration is a class-level annotation that you can use to declare that the ApplicationContext loaded for an integration test should be a WebApplicationContext. The resource base path is used behind the scenes to create a MockServletContext.
      • @ActiveProfiles is a class-level annotation that is used to declare which bean definition profiles should be active when loading an ApplicationContext for an integration test.
      • @TestPropertySource is a class-level annotation that you can use to configure the locations of properties files and inlined properties to be added to the set of PropertySources in the Environment for an ApplicationContext loaded for an integration test.
      • @DirtiesContext indicates that the underlying Spring ApplicationContext has been dirtied during the execution of a test and should be closed.
    • Standard Annotation Support:
      • This category of annotations you can use in the tests and around all the Spring application as @Autowired, @Qualifier, @Value, etc.
    • Spring JUnit 4 Testing Annotations
      • Annotations supported when used with SpringRunner, Spring’s JUnit 4 rules, or Spring’s JUnit 4 support classes: @Timed, @Repeat, @IfProfileValue, @ProfileValueSourceConfiguration
    • Spring JUnit Jupiter Testing Annotations
      • @SpringJUnitConfig is a composed annotation that combines @ExtendWith(SpringExtension.class) from JUnit Jupiter with @ContextConfiguration from the Spring TestContext Framework. It can be used at the class level as a drop-in replacement for @ContextConfiguration.
      • @SpringJUnitWebConfig is a composed annotation that combines @ExtendWith(SpringExtension.class) from JUnit Jupiter with @ContextConfiguration and @WebAppConfiguration from the Spring TestContext Framework. You can use it at the class level as a drop-in replacement for @ContextConfiguration and @WebAppConfiguration.


    Hands On

    I'll present you short examples of different tests. The complete code you can access here.

  • Unit Test
  • Here is one example of a unit test using mocks to simulate a service. In this case, you describe the action should be done when the service is called.

    @SpringJUnitConfig
    public class StudentServiceTest {
    
      @Mock
      private StudentRepository studentRepository;
      @InjectMocks
      private StudentService service;
    
      @Test
      void getStudentById_forSavedStudent_isReturned() {
    
        // given
        Student student = new Student(1L,"Joao", true, 100);
        Mockito.when(studentRepository.findById(1L))
          .thenReturn(Optional.of(student));
    
        // when
        Student result = service.getStudentById(1L);
    
        // then
        assertThat(result.getName()).isNotNull();
        assertThat(result.getName()).isEqualTo("Joao");
    
      }
    }


  • Integration Test
  • Here is an example of an integration test going through the service. The navigation really go inside the service and repository classes. Pay attention to the class annotation and the services annotation. They are different.

    @SpringBootTest (webEnvironment = WebEnvironment.NONE)
    @Transactional
    public class StudentServiceTest {
    
      @Autowired
      private StudentRepository studentRepository;
      @Autowired
      private StudentService studentService;
    
      @DisplayName("Returning saved student from service layer")
      @Test
      void getStudentById_forSavedStudent_isReturned() {
    
        // given
        Student savedStudent = studentRepository
          .save(new Student(null, "Joao", true, 100));
    
        // when
        Student student = studentService.getStudentById(savedStudent.getId());
    
        // then
        then(student.getName()).isEqualTo("Joao");
        then(student.getId()).isNotNull();
    
      }
    }


  • Rest Test
  • Here we have an example how to test an rest service using TestRestTemplate.

    @Autowired
    private TestRestTemplate restTemplate;
    
    @Autowired
    private StudentRepository studentRepository;
    
    @Test
    public void shouldRetrieveStudent() throws JsonProcessingException {
    
      Student savedStudent = studentRepository
                .save(new Student(null, PARAM_QUERY, true, 100));
    
      ResponseEntity<Student> response = restTemplate
                .getForEntity("/students/" + PARAM_QUERY, Student.class);
    
      assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
      assertThat(response.getBody().getId()).isEqualTo(savedStudent.getId());
    }

    You can see another example here but using RestClientTest and MockRestServiceServer. It is a more verbose solution and is necessary to know more detail about the implementation.


  • Caching Test
  • This example verify if your application is using cache. To make it works your main class application should be using the annotation @EnableCaching and the method of the service should be using the @Cacheable("student") annotation. Pay attention now is using @MockBean to the repository. You can see a discussion about use @Mock or @MockBean you can see here.

    @SpringBootTest(webEnvironment = WebEnvironment.NONE)
    public class StudentCacheTest {
    
      @Autowired
      private StudentService service;
    
      @MockBean
      private StudentRepository repository;
    
      @Test
      void getStudentById_forMultipleRequests_isRetrievedFromCache() {
    
        Long id = 123L;
        given(repository.findById(id)).willReturn(Optional.of(new Student(1L,"Joao", true, 100) ));
    
        service.getStudentById(id);
        service.getStudentById(id);
        service.getStudentById(id);
    
        then(repository).should(times(1)).findById(id);
    
      }
    }


  • Tests with Database
  • This kind of test use in-memory databases. The data should be loaded before the execution. In this case, is not necessary to use the SpringBooTest because it doesn't need all the application context. Then, the @DataJpaTest annotation is enough.

    The scenario wants to test the attribute name. The test uses a bean given by Spring, the TestEntityManager. It loads the data before the test. The commented line indicates how NOT to do. The direct use of your repository object will give a wrong answer of the test because the repository object are interacting with the first level cache of jpa repository. The TestEntityManager test entity manager to flush data and make possible the test. To works correctly, the entity Student has to have a default constructor inside the entity class.

    @DataJpaTest
    public class StudentRepositoryTest {
    
      @Autowired
      private StudentRepository studentRepository;
      @Autowired
      private TestEntityManager testEntityManager;
    
      @Test
      void testGetObjectByName_ReturnObj() {
    
        // given
        #Student savedStudent = studentRepository.save(new Student(null, "Joao"));
        Student savedStudent = testEntityManager.persistFlushFind(new Student(null, "Joao", true, 0));
    
        // when
        Student student = studentRepository.getStudentByName("Joao");
    
        // then
        then(student.getId()).isNotNull();
        then(student.getName()).isEqualTo(savedStudent.getName());
      }
    }

    Also, is possible to use the annotation @Sql to define files with queries to prepare the in-memory database to the tests.

    When the SQL annotation is on the class declaration then the scripts will run before each @Test method. The Sql on the method has priority over the Sql of the class.

    @SpringIUnitConfig(...)
    @Sql({"/scripts/schema.sql", "/scripts/load-data.sql"})
    public class MyTest{
      @Test
      @Sql("/scripts/test-data.sql")
      @Sql("/scripts/cleanup-data.sql", executionPhase=Sql.ExecutionPhase.AFTER_TEST_METHOD)
      public void insertNewRecord(){
        ...
      }
    }


    References