Block Image

In the previous post (TDD and Unit Test), we saw how to implement a unit test using jUnit 5 and Mockito.
In this new post we will cover instead the Integration Test part exploiting the potential of Spring Boot always using the Test-Driven Development. We will hypothesize to use a MySQL database to store the users (you can follow the instructions here to create a Docker container of MySQL: Container Docker MySQL).
For integration tests instead, we will use H2, an in-memory database. Why? A good test must be self-consistent, it must work for whoever performs it without binding database and application server installations for proper operation.

We resume the requirements:

«We are asked to implement REST APIs of findById and create of a User model.
In particular, in findById, the client must receive a 200 and in the response body the JSON of the User found.
If there is no User with the input id, the client must receive 404 with an empty body.
Moreover the client sees 2 fields of the User: name and address; the name is composed by the last name, a space and the first name.»

Maven Dependencies to import

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.4.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
...

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

First step: modify the application.properties

spring.datasource.url=jdbc:mysql://localhost:3306/users_DB?useSSL=false&serverTimezone=Europe/Rome
spring.datasource.username=root
spring.datasource.password=root

spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.database-platform=org.hibernate.dialect.MySQL57Dialect

We use the property spring.jpa.hibernate.ddl-auto=create-drop to inform Hibernate that it will be responsible for creating the tables in the database (we have to create the users_DB database).

Secondo passo: creiamo il controller

@RestController
@RequestMapping("/users")
public class UserController {

    private UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/{id}")
    public UserDTO findById(@PathVariable Long id) {
        return null;
    }
}

Initially findById will return null; we will implement the method after creating the test class.

Third step: let's create the application.properties for the tests

spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

spring.jpa.hibernate.ddl-auto=create-drop

If Spring Boot finds an application.properties file in src/test/resources, it will use that one, otherwise it will consider the one located in src/main/resources.

Fourth step: let's create the test class for UserController

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UserControllerTest {

    @LocalServerPort
    private int port;

    private String baseUrl = "http://localhost";


    private static RestTemplate restTemplate = null;

    @BeforeAll
    public static void init() {
        restTemplate = new RestTemplate();
    }

    @BeforeEach
    public void setUp() {
        baseUrl = baseUrl.concat(":").concat(port+ "").concat("/users");
    }

    @Test
    @Sql(statements = "INSERT INTO user (id, name, surname, address) VALUES (1, 'Vincenzo', 'Racca', 'via Roma')",
            executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
    @Sql(statements = "DELETE FROM user",
            executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
    public void returnAPersonWithIdOne() {

        UserDTO userDTOResponse = restTemplate.getForObject(baseUrl.concat("/{id}"), UserDTO.class, 1);
        assertAll(
                () -> assertNotNull(userDTOResponse),
                () -> assertEquals("Racca Vincenzo", userDTOResponse.getName())
        );
    }
}

Let's analyze the code:

  1. @SpringBootTest allows us to load the Spring context. Moreover with SpringBootTest.WebEnvironment.RANDOM_PORT we say that the port of the Web Service during the test must be chosen randomly so that the tests do not collide with each other.

  2. @LocalServerPort allows us to inject the random port value into the port variable.

  3. @BeforeAll allows us to initialize the variable type RestTemplate (which is a Spring REST client), before the methods of the Test class are executed. So in practice, when you run all the tests of the UserControllerTest class, the init method will be called only once. Also @BeforeAll matches to JUnit 4's @BeforeClass annotation.

@BeforeEach allows us to initialize the baseUrl variable every time a method of the test class is called. So when we run all the tests of the UserControllerTest, the setUp method will be called as many times as there are methods in the class. It corresponds to @Before of Junit 4. 5) @Sql allows us to do some operations to the test db, like statement sql or even read a .sql file. As we can see, we can annotate several times the same method with this annotation. In the first one, we make an INSERT; executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD guarantees that the statement is performed before the method is performed. In the second one, we delete the method after executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD.

Let's analyze the test:
We call localhost:{port}/users/1 to the Web Service using RestTemplate and we expect the service to return a UserDTO with name Racca Vincenzo.
Obviously the test fails already at the first assertion because the service returns null.

Note 1: Instead of executing a delete after the method is executed, a more elegant way to undo the DB changes made by a test method is to note the method with @Transactional. In fact Spring, when it finds a test with this annotation, it automatically runs the rollback of changes when the method ends.

Fifth step: let's fix the method

@GetMapping("/{id}")
public UserDTO findById(@PathVariable Long id) {
    return userService.findById(id);
}

The test is now passed!

Sixth step: we test the method with an invalid input

@Test
public void return404() {
    ResponseEntity<String> err = restTemplate.getForEntity
            (baseUrl.concat("/{id}"), String.class, 1);

    assertAll(
            () -> assertNotNull(err),
            () -> assertEquals(HttpStatus.NOT_FOUND, err.getStatusCode()),
            () -> assertNull(err.getBody())
    );
}

We also adjust the RestTemplate initialization because we don't want the custom error handler of this class to be handled but rather a handler created by us in the application is invoked.
The test fails in the last 2 assertions:

expected: <404 NOT_FOUND> but was: <500 INTERNAL_SERVER_ERROR>
Comparison Failure: 
Expected :404 NOT_FOUND
Actual   :500 INTERNAL_SERVER_ERROR

expected: <null> but was: <{"timestamp":"2020-10-04T17:21:37.644+00:00","status":500,"error":"Internal Server Error","message":"","path":"/users/1"}>
Comparison Failure: 
Expected :null
Actual   :{"timestamp":"2020-10-04T17:21:37.644+00:00","status":500,"error":"Internal Server Error","message":"","path":"/users/1"}

The test highlights the fact that the findById method currently does not meet this part of the requirement:
¨If there is no User with the given input id, the client must receive 404 with an empty body.¨

Step 7: Let's fix the method once again

Within the UserController class, we write a method that handles the UserNotFoundException exception:

@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler({UserNotFoundException.class})
public void handleNotFound() {
    // just return empty 404
}

With this method we are saying that every service that launches the UserNotFoundException exception, returns a HttpStatus 404 with a null body. Let's run the test again, this time it's passed!

Note 2: A more elegant way to handle exceptions in a Web Service REST is to create an error handler class.

Conclusions

We have seen how to carry out an Integration Test using Spring Boot in addition to JUnit 5 and H2, continuing to use the Test-Driven Development.

You can download the full project with also create test, and an Integration Test for the Service from my github in this link: Spring Test

Previous post on Unit Test: TDD e Unit Test

Posts of Spring Framework: Spring

Recommended books about Spring:

  • Pro Spring 5 (Spring from scratch a hero): https://amzn.to/3KvfWWO
  • Pivotal Certified Professional Core Spring 5 Developer Exam: A Study Guide Using Spring Framework 5 (for Spring certification): https://amzn.to/3KxbJSC
  • Pro Spring Boot 2: An Authoritative Guide to Building Microservices, Web and Enterprise Applications, and Best Practices (Spring Boot of the detail): https://amzn.to/3TrIZic