This article shows an example of Integration Testing of RESTful Web Services using Spring Boot’s test framework support. It uses @SpringBootTest annotation to load an EmbeddedWebApplicationContext and provide a real servlet environment. The in-memory database is used to perform database operations.
This example shows integration tests of RESTful Web Services of CRUD operations explained in the earlier article.
Technology Stack
Technology stack used in this example is:
- Spring Boot 1.4.1.RELEASE
- Spring Data JPA
- Database – PostgreSQL, HSQLDB
- JDK 8
RESTful Web Services Unit Testing with Spring Boot
RESTful Web Services CRUD operations using Spring Boot
RESTful Web Services Authentication and Authorization
Integration Testing
Before starting, something about integration testing. As the name suggests, in integration testing we have to test integration of all layers of the application. The environment of integration testing should be as similar as possible to the actual application environment where it supposed to run. Unlike unit testing, all application layers should communicate with each other. For example, data should be sent from Web layer to DAO layer(through Service layer) and hit the actual database. Most integration tests are written for the top layer like Web, and expects a proper response after processing through Service and DAO layer.
The application under test is a RESTful Web Service for CRUD operations using Spring Boot and Spring Data JPA. It uses PostgreSQL as a database. The application is planned to deploy on tomcat server.
To perform integration tests for this application we will create the following environment:
- Deploy the application on Embedded Tomcat Server on random port
- Use HSQLDB in-memory database
- Initialize the database with some data for testing
- Use TestRestTemplate from Spring Boot to call the Restful Web Services
- No mocking or stubbing of any operation
Now take a look at project configuration to achieve all above things.
Project Setup
You just need two additional dependencies in our RESTFul Web Service project’s pom.xml to start writing integration test cases.
- spring-boot-starter-test : Spring Boot Test Framework with libraries including JUnit, Mockito etc.
- Library for HSQLDB database (org.hsqldb::hsqldb)
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.bytestree.restful</groupId> <artifactId>spring-restful-service-integration-test</artifactId> <version>1.0.0-SNAPSHOT</version> <packaging>jar</packaging> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.4.1.RELEASE</version> <relativePath /> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <dependencies> <!-- Spring Boot --> <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> <!-- database --> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> </dependency> <!-- Testing --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.hsqldb</groupId> <artifactId>hsqldb</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
Apart from pom.xml changes we also need two additional files:
init-data.sql
This SQL file contains SQL commands to initialize the data required for test cases. We added two SQL statements. First to truncate the employee table and second to insert one record of the employee. “delete” and “get” operations will use this record in the test case.
TRUNCATE TABLE employee; INSERT INTO employee (id, designation, firstname, lastname, salary) VALUES (1, 'devloper', 'Bytes', 'Tree-Init', '1100');
test.properties
This is property file for testing. Here specify the database connection details for testing. Most of the cases it is an in-memory database like HSQLDB, H2 etc. If you want you can create a separate test instance of the database used by an application. You can also specify different logging levels for testing. In our example, debug level is set for RestTemplate class to check the sent requests.
# Server server.contextPath = /rest # Database spring.datasource.driverClassName=org.hsqldb.jdbcDriver spring.datasource.url=jdbc:hsqldb:mem:testdb spring.datasource.username=sa spring.datasource.password= # JPA spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.HSQLDialect spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.show-sql=true # Logging logging.level.com.bytestree.restful=DEBUG logging.level.org.springframework.web.client.RestTemplate=DEBUG
Project structure will be as follows:
Integration Test Class Configuration
Let’s start writing the Test class responsible for integration testing of our RESTful Web Service. First, take a look at class level configuration:
//import statements @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @TestPropertySource(locations = "classpath:test.properties") @Sql({ "classpath:init-data.sql" }) public class EmployeeControllerTest { @Autowired private TestRestTemplate restTemplate; private static final String URL = "/employee/"; // test methods }
Take a look at each configuration one by one:
@RunWith(SpringRunner.class)
: It is an alias for SpringJUnit4ClassRunner . It will add Spring TestContext Framework support.@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
: This will create applicationContext for testing. The weEnvironment property configured to WebEnvironment.RANDOM_PORT loads an EmbeddedWebApplicationContext and provides real servlet environment. It starts an embedded servlet container at random(free) port. This also registers the TestRestTemplate bean to make a rest call.@TestPropertySource(locations = "classpath:test.properties")
: This loads the application properties for testing from the specified location. Here we configure the HSQLDB connection properties. You may create a separate instance of PostgreSQL database(or any other) for testing. You may also put some additional log configurations for testing. Like setting DEBUG logging level for RestTemplate in our example.@Sql({ "classpath:init-data.sql" })
: Used to load the initial data for test database from SQL file in the classpath. Since this is specified at the class level, it will execute before each test case.restTemplate
: This is an autowired instance of TestRestTemplate registered by @SpringBootTest.
Writing Integration Test Cases
Integration testing will follow same steps as unit testing explained here. Those steps are Preparation, Execution, and Verification. The only difference is we no longer require to mock any layer’s behavior. So preparation step only requires for Add and Update operation to create the object to Add/Update. Now, let’s start with first Integration test.
Integration Test for Get operation
The method under test:
/** * Get the Employee by ID * * @param id of Employee * @return Employee */ @RequestMapping(value = "/{id}", method = RequestMethod.GET) public ResponseEntity<Employee> getEmployee(@PathVariable("id") Long id) { Employee employee = empService.getById(id); if (employee == null) { logger.debug("Employee with id " + id + " does not exists"); return new ResponseEntity<Employee>(HttpStatus.NOT_FOUND); } logger.debug("Found Employee:: " + employee); return new ResponseEntity<Employee>(employee, HttpStatus.OK); }
Below are the key steps this method is performing:
- To get the Employee it makes one call to Service layer which is empService.getById(id)
- It returns ResponseEntity with Employee object with HTTPStatus OK / NOT_FOUND based on service response.
We need to verify these things in our integration test. So an integration test should:
- Sent GET request with “id” parameter set to employee id to find.
- Check the HTTPStatus of response.
- Check the returned object.
To send the GET request we use getForEntity() method of TestRestTemplate already autowired in test class. ResponseEntity’s getStatusCodeValue() and getBody() will provide the status and returned object.
We can have two test cases here. First with a positive scenario when an employee is found and another negative scenario when the employee is not found. And here is the code snippet of these two test cases.
Test Cases:
@Test public void testGetEmployee() throws Exception { // prepare // Not required as init-data.sql will insert one record which will // be retrieved here // execute ResponseEntity<Employee> responseEntity = restTemplate.getForEntity(URL + "{id}", Employee.class, new Long(1)); // collect response int status = responseEntity.getStatusCodeValue(); Employee resultEmployee = responseEntity.getBody(); // verify assertEquals("Incorrect Response Status", HttpStatus.OK.value(), status); assertNotNull(resultEmployee); assertEquals(1l, resultEmployee.getId().longValue()); }
@Test public void testGetEmployeeNotExist() throws Exception { // prepare data and mock's behaviour // Not Required as employee Not Exist scenario // execute ResponseEntity<Employee> responseEntity = restTemplate.getForEntity(URL + "{id}", Employee.class, new Long(100)); // collect response int status = responseEntity.getStatusCodeValue(); Employee resultEmployee = responseEntity.getBody(); // verify assertEquals("Incorrect Response Status", HttpStatus.NOT_FOUND.value(), status); assertNull(resultEmployee); }
Note: Check the source code for the test case of “getAll” operation.
All other test cases will follow above way of testing. Make a rest call using TestRestTemplate and verify the ResponseEntity object. Below are the code snippets for rest of the methods.
Integration Test for Update operation
The method under test:
/** * Update existing employee * * @param employee * @return void */ @RequestMapping(method = RequestMethod.PUT) public ResponseEntity<Void> updateEmployee(@RequestBody Employee employee) { Employee existingEmp = empService.getById(employee.getId()); if (existingEmp == null) { logger.debug("Employee with id " + employee.getId() + " does not exists"); return new ResponseEntity<Void>(HttpStatus.NOT_FOUND); } else { empService.save(employee); return new ResponseEntity<Void>(HttpStatus.OK); } }
Test Case:
@Test public void testUpdateEmployee() throws Exception { // prepare // here the create the employee object with ID equal to ID of // employee need to be updated with updated properties Employee employee = new Employee(1l, "bytes", "tree", "developer", 15000); HttpEntity<Employee> requestEntity = new HttpEntity<Employee>(employee); // execute ResponseEntity<Void> responseEntity = restTemplate.exchange(URL, HttpMethod.PUT, requestEntity, Void.class); // verify int status = responseEntity.getStatusCodeValue(); assertEquals("Incorrect Response Status", HttpStatus.OK.value(), status); }
A thing to note in above test case is exchange method of TestRestTemplate is used to make rest call with PUT HttpMethod. This is because our method under test returns a ResponseEntity with HttpStatus. If your Update method is void, you can use put(URI url, Object request) method of TestRestTemplate.
Integration Test for Delete operation
The method under test:
/** * Delete Employee * * @param id of Employee to delete * @return void */ @RequestMapping(value = "/{id}", method = RequestMethod.DELETE) public ResponseEntity<Void> deleteEmployee(@PathVariable("id") Long id) { Employee employee = empService.getById(id); if (employee == null) { logger.debug("Employee with id " + id + " does not exists"); return new ResponseEntity<Void>(HttpStatus.NOT_FOUND); } else { empService.delete(id); logger.debug("Employee with id " + id + " deleted"); return new ResponseEntity<Void>(HttpStatus.GONE); } }
Test Case:
@Test public void testDeleteEmployee() throws Exception { // execute - delete the record added while initializing database with // test data ResponseEntity<Void> responseEntity = restTemplate.exchange(URL + "{id}", HttpMethod.DELETE, null, Void.class, new Long(1)); // verify int status = responseEntity.getStatusCodeValue(); assertEquals("Incorrect Response Status", HttpStatus.GONE.value(), status); }
Similar to update, exchange method is used to make rest call because our method under test returns ResponseEntity. You may have a void method in Controller to perform the delete operation. In that case, you can use delete(URI url) method of TestRestTemplate.
Integration Test for Add operation
The method under test:
/** * Add new employee * * @param employee * @return Added Employee */ @RequestMapping(method = RequestMethod.POST) public ResponseEntity<Employee> addEmployee(@RequestBody Employee employee) { empService.save(employee); logger.debug("Added:: " + employee); return new ResponseEntity<Employee>(employee, HttpStatus.CREATED); }
Test Case:
@Test public void testAddEmployee() throws Exception { // prepare Employee employee = new Employee("bytes", "tree", "developer", 12000); // execute ResponseEntity<Employee> responseEntity = restTemplate.postForEntity(URL, employee, Employee.class); // collect Response int status = responseEntity.getStatusCodeValue(); Employee resultEmployee = responseEntity.getBody(); // verify assertEquals("Incorrect Response Status", HttpStatus.CREATED.value(), status); assertNotNull(resultEmployee); assertNotNull(resultEmployee.getId().longValue()); }
Hope you have got all basics of writing the integration test for Restful web services using Spring Boot.
Source Code
The complete source code for Integration testing of all CRUD operations in RESTful Web Services is on GitHub