End-To-End Testing Spring Boot REST APIs with Rest Assured

Sielei Herman
6 min readMar 25, 2024

--

Testing has proved to be a very crucial process that ensures the delivery of high-quality software. A comprehensive test strategy would incorporate unit tests, integration tests, end-to-end tests, and user acceptance testing. In this article, we review how we can use Rest Assured to perform a REST API End-to-End Test for a Spring Boot application.

Rest Assured

Rest Assured is a Java library for testing and validating Restful web services. It can test both XML and JSON-based web services. In Rest Assured these three methods are crucial for writing tests:

  • given() Is used to specify request headers, body, cookies, and authentication.
  • when() Is used to specify the HTTP method (GET, POST, PUT, PATCH, DELETE) of the request.
  • then() Contains the response to be validated.

Here is a code sample using Rest Assured to set the authentication header, specifying a JSON request body, specifying the HTTP method as POST, and validating the response status code and body.

@Test
void shouldCreateArticle(){
given().contentType(ContentType.JSON)
.body("""
{
"name": "Test Title"
}
"""
)
.header("Authorization", "Bearer " + token)
.when()
.post("/api/articles")
.then()
.statusCode(201)
.body("id", notNullValue())
.body("name", equalsTo("Test Title"));
}

Rest Assured uses Hamcrest matchers such as notNullValue() to assert that the response body and headers match the expected values.

Adding Rest Assured dependency to Maven

To add Rest Assured to your maven Spring Boot project, copy and paste the following into your pom.xml file.

<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>5.4.0</version>
<scope>test</scope>
</dependency>

Adding Rest Assured dependency to gradle

To add Rest Assured to your gradle Spring Boot project, copy and paste the following into your build.gradle file.

testCompile('io.rest-assured:rest-assured:5.4.0')

Spring Boot Application

For this demonstration, we will be testing a Spring Boot Application that serves the following REST endpoints:

Swagger UI for API documentation

The code for this application and setup instructions can be found on GitHub. This application contains a REST controller similar to the one shown below. The end-to-end test will mainly be testing the methods in this class since requests to various API endpoints are handled by methods in this class.

@RestController
@RequestMapping("/api/v1/jobs")
public class JobsController {
private final JobService jobService;

public JobsController(JobService jobService) {
this.jobService = jobService;
}

@PostMapping
ResponseEntity<JobResponse> addNewJob(@Valid @RequestBody JobRequest jobRequest){
var newJob = jobService.addNewJob(jobRequest);
var location = ServletUriComponentsBuilder.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(newJob.id())
.toUri();
return ResponseEntity.created(location).body(newJob);
}

@GetMapping
PagedContent<JobResponse> findAllJobs(@RequestParam(name = "page", defaultValue = "1") Integer pageNo,
@RequestParam(name = "size", defaultValue = "10") Integer pageSize){
return jobService.findAllJobs(pageNo, pageSize);
}

@GetMapping("/{jobId}")
JobResponse findById(@PathVariable("jobId") Long jobId){
return jobService.findJobById(jobId);
}

@PutMapping("/{jobId}")
@ResponseStatus(HttpStatus.NO_CONTENT)
void updateJob(@PathVariable("jobId") Long jobId, @Valid @RequestBody JobRequest updateJobRequest){
jobService.updateJob(jobId, updateJobRequest);
}

@DeleteMapping("/{jobId}")
@ResponseStatus(HttpStatus.NO_CONTENT)
void deleteJob(@PathVariable("jobId") Long jobId){
jobService.deleteJob(jobId);
}

}

Setting Up the Test Class

We will set up a JobsControllerTest class to use testcontainers for integration testing with PostgreSQL. This will allow us to run an end-to-end test of our REST APIs including a database integration test.

Since the Spring Boot Project uses flyway for database migrations, testcontainers will set up the database container, create a database, and trigger flyway migration. This will ensure that we have data to perform tests especially when performing GET, PUT, and DELETE requests.

Database migrations with flyway and testcontainers for integration testing are beyond the scope of this article so we will not discuss them further.

To set up the test class, create a Java class called JobsControllerTest in the test package of your Spring Boot and add the following code.

@SpringBootTest(webEnvironment = RANDOM_PORT)
@Testcontainers
class JobsControllerTest {

@Container
@ServiceConnection
static PostgreSQLContainer<?> postgreSQLContainer = new PostgreSQLContainer<>(
DockerImageName.parse("postgres:latest")
);

@LocalServerPort
private Integer port;

@Autowired
JobService jobService;

@BeforeEach
void setUp() {
RestAssured.port = port;
}
}

We use the @SpringBootTest annotation so that we can load the whole application context. The @Testcontainers annotation tells Spring Boot to run this class with testcontainers.

To effectively use Rest Assured we have to add the following static imports to our test class.

io.restassured.RestAssured.*
io.restassured.matcher.RestAssuredMatchers.*
org.hamcrest.Matchers.*

Testing POST endpoint: Add new Job Post

Add the following code snippet to the JobsControllerTest class.

@Test
void shouldAddNewJobPost(){
given().contentType(ContentType.JSON)
.body("""
{
"jobTitle": "Senior Backend Engineer (Java)",
"description": "My Company is looking for an experienced Java Backend developer.",
"jobType": "FULL_TIME",
"datePosted": "2024-03-24",
"jobLink": "https://mycompany.breezy.hr/p/22b17727d5f4-senior-backend-engineer-java"
}
"""
)
.when()
.post("/api/v1/jobs")
.then()
.statusCode(201)
.header("Location", matchesRegex(".*/api/v1/jobs/[0-9]+$"))
.body("id", notNullValue())
.body("jobTitle", equalTo("Senior Backend Engineer (Java)"))
.body("jobLink", equalTo("https://mycompany.breezy.hr/p/22b17727d5f4-senior-backend-engineer-java"));
}

In this test, we use given() method to specify that our request will contain a JSON body and we use Java text blocks to define the request JSON. We then use when() method to specify that we will be making a POST request to /api/v1/jobs route. Requests to this route are handled by addNewJob() method in our REST controller. Using then() we get the ValidatableResponse from which we validate the response status code, header, and body. Here we are using matchRegex(), notNullValue(), and equalTo() Hamcrest matchers to assert that the response body field values and header values match the expected values.

Testing GET endpoint: Find Job post by ID

Add the following snippet to the JobsControllerTest class.

@Test
void shouldFindJobPostById(){
when()
.get("/api/v1/jobs/{jobId}", 10)
.then()
.statusCode(200)
.body("id", equalTo(10))
.body("jobTitle", equalTo("Speech Pathologist"))
.body("datePosted", equalTo("2024-02-19"));
}

In this test, we use when() to specify that we will be making a GET request to /api/v1/jobs/{jobId} route. We pass the value 10 as the ID for the job posting we are fetching. Note that {jobId} acts as a placeholder for job ID.

Using then() method we can validate the request body using Hamcrest matcher equalTo() to assert that the body fields match expected values.

Test GET endpoint: Find All available Job posts

Add the following code snippet to the JobsControllerTest class.

@Test
void shouldFindAllAvailableJobPosts(){
given()
.queryParam("page", 2)
.queryParam("size", 50)
.when()
.get("/api/v1/jobs")
.then()
.statusCode(200)
.body("totalElements", equalTo(1000))
.body("pageNumber", equalTo(2))
.body("totalPages", equalTo(20))
.body("isFirst", equalTo(false))
.body("isLast", equalTo(false))
.body("hasNext", equalTo(true))
.body("hasPrevious", equalTo(true))
.body("data.size()", equalTo(50));
}

This test is very similar to the previous one where we find Job posts by ID. However, since our endpoint is paginated we need to specify a page and size query parameters to the request. Rest Assured provides the queryParam() method that allows us to specify query parameters.

Testing PUT endpoint: Update a Job Post

Add the following code snippet to the JobsControllerTest class.

@Test
void shouldUpdateJobPost(){
given().contentType(ContentType.JSON)
.body("""
{
"jobTitle": "Electrical Engineer",
"description": "XYZ inc. Is looking for an experienced Electrical Engineer.",
"jobType": "CONTRACTOR",
"datePosted": "2023-09-18",
"jobLink": "https://xyz.bamboohr.hr/p/22b177hg675-electrical-engineer"
}
"""
)
.when()
.put("/api/v1/jobs/{jobId}", 3)
.then()
.statusCode(204);
}

In this test, we do not validate the response body since our endpoint returns no content on a successful update. Therefore we only validate that the response status code is correct.

Testing DELETE endpoint: Delete a Job Post

Add the following code snippet to the JobsControllerTest class.

@Test
void shouldDeleteJobPost(){
when()
.delete("/api/v1/jobs/{jobId}", 50)
.then()
.statusCode(204);

assertThatExceptionOfType(JobNotFoundException.class).isThrownBy(
() -> jobService.findJobById(50L))
.withMessage("Job with id: 50 not found!");
}

In this test, we send a DELETE request for a Job post with an ID of 50. We then validate the response status code is 204 after which we write an assertion that verifies an exception should be thrown when we try to find the deleted Job post.

Conclusion

We have seen how to perform end-to-end tests for Spring Boot REST APIs using Rest Assured. To learn more about Rest Assured you can visit the Rest Assured usage guide.

The code samples can be found on this GitHub link. Feel free to fork or clone the repository and try it out.

--

--