Test-Driven Development With a Spring Boot REST API
While TDD starts with unit tests, it doesn't stop there. Let's look at how integration tests come into play with a Spring Boot-based REST API.
Join the DZone community and get the full member experience.
Join For FreeI deal with integration tests for RESTful applications a lot, however, I had not particularly tried Test Driven Development (TDD) methodologies. Therefore, I decided to give it a try, and I say now tell that I quite like it. I shall assume you already have some basic ideas of TDD, therefore I shall forgo an introduction to TDD.
In this article, let us look at how we can adopt TDD methodology in implementing a Spring Boot RESTful application.
Creating a Spring Boot Application
To start an application with Spring Boot is really easy; we just need to include Spring Boot dependencies in the Maven pom.xml and we need a Java class file.
Create a simple Maven project in the IDE of your choice. I have created mine in Eclipse. The project structure is as follows:
The Maven POM contains the following:
<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.swathisprasad.tdd</groupId>
<artifactId>tdd-springboot</artifactId>
<version>1.0.0-SNAPSHOT</version>
<!—Spring Boot starter parent goes here -->
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
</project>
Edit the pom.xml and add the following Spring Boot dependencies. Here, we are adding spring-boot-starter-parent as a parent for our project.
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
Under the dependencies section, add the following dependency.
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
In combination with spring-boot-starter-parent, the spring-boot-starter-web dependency allows us to run the web application.
Here is the class which is responsible for running the application.
package com.swathisprasad.springboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author swathi
*
*/
@SpringBootApplication
public class Application {
/**
* @param args
*/
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
So far we have created a simple Spring Boot application. We can test our application by running Application.java as a Java application.
As we are focusing on TDD methodologies, let us first create a simple integration test. Edit the pom.xml file and add the following dependencies.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jayway.restassured</groupId>
<artifactId>rest-assured</artifactId>
<version>2.8.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>2.4.5</version>
</dependency>
Here, spring-boot-starter-actuator provides production ready feature such as monitoring our app, gathering metrics and so on. The spring-boot-starter-test allows to bootstrap Spring context before executing tests.
The Maven plugin rest-assured is a Java DSL for testing REST services. This plugin requires groovy-all to run the tests.
We will add maven-failsafe-plugin plugin to execute the integration tests.
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<executions>
<execution>
<id>integration-test</id>
<goals>
<goal>integration-test</goal>
</goals>
</execution>
<execution>
<id>verify</id>
<phase>verify</phase>
<goals>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<id>pre-integration-test</id>
<goals>
<goal>start</goal>
</goals>
</execution>
<execution>
<id>post-integration-test</id>
<goals>
<goal>stop</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
Here, we have also added spring-boot-maven-plugin with some execution tasks in order to execute the tests during the deployment process.
Let's create a test which makes a simple GET request and expects data in the response body.
package com.swathisprasad.springboot.controller;
import static com.jayway.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.swathisprasad.springboot.Application;
import com.jayway.restassured.RestAssured;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
/**
* @author swathi
*
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = Application.class)
@TestPropertySource(value={"classpath:application.properties"})
@SpringBootTest(webEnvironment = WebEnvironment.DEFINED_PORT)
public class SpringRestControllerTest {
@Value("${server.port}")
int port;
@Test
public void getDataTest() {
get("/api/tdd/responseData").then().assertThat().body("data", equalTo("responseData"));
}
@Before
public void setBaseUri () {
RestAssured.port = port;
RestAssured.baseURI = "http://localhost"; // replace as appropriate
}
}
Note that we have added some annotations here to run the tests in a web environment.
@RunWith(SpringJUnit4ClassRunner.class) supports loading of the Spring application context.
@ContextConfiguration defines class-level metadata that is used to determine how to load and configure an ApplicationContext for integration tests.
@TestPropertySource is used to configure the location of properties files.
SpringBootTest tells Spring Boot to go and look for a main configuration class (one with @SpringBootApplication, for instance) and use that to start a Spring application context.
As we have not defined a REST endpoint yet, we can run the above test using the mvn verify
command to see the test fail.
Let's fix our test by introducing a REST controller.
package com.swathisprasad.springboot;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
/**
* @author swathi
*
*/
@RestController
public class SpringRestController {
@RequestMapping(path = "/api/tdd/{data}", method= RequestMethod.GET)
public Response getData(@PathVariable("data") String data) {
return new Response(data);
}
//inner class
class Response {
private String data;
public Response(String data) {
this.data = data;
}
public String getData() {
return data;
}
}
}
In the class above, we have a REST endpoint which accepts "data" as an input and returns a new instance of "Response" in the response body.
Let's run the mvn verify
command again. It should now run the tests successfully.
Wrapping Up
Developing our implementation with TDD enables us to define the requirements in the form of tests. This approach also helps to maintain our code to make it easier to read and maintain while still passing the tests.
The complete source code can be found on GitHub.
Published at DZone with permission of Swathi Prasad, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments