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:
@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.@LocalServerPort allows us to inject the random port value into the port variable.
@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.
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!
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