Integration testing Rest services in springboot

Posted by Somesh Shinde On Saturday, 20 May 2017 0 comments
Integration testing Rest services in springboot

This guide will help you write great integration tests for your Spring Boot Rest Service. We will use a simple code example creating couple of simple rest services.

You will learn

  • What is Integration Testing?
  • How to create a Get REST Service for retrieving the courses that a student registered for?
  • How to write a integration test for Get REST Service?
  • How to create a Post REST Service for registering a course for student?
  • How to write a integration test for the POST Service?

Tools you will need

  • Maven 3.0+ is your build tool
  • Your favorite IDE. We use Eclipse.
  • JDK 1.8+

Integration Testing

Following screenshot shows eclipse project with all the files we will create.
Image
We want to create a integration test for StudentController which is a Rest Controller. StudentController exposes two service methods - one Get and one Post. We will write integration tests for both these service methods.
In the integration test
  • We will launch the complete Spring Boot application using @SpringBootTest
  • We will invoke the service methods using TestRestTemplate
  • We will assert the results using a great JSON assert framework - org.skyscreamer.jsonassert.JSONAssert
A key part of integration testing is testing all the layers in the application.

Overview

Following is the order we do things in this guide:
  • Bootstrap a project using Spring Initializr.
  • Implement a Business Service for our API - StudentService.
  • Implement the API - using StudentController. First we implement the GET methods and then the POST methods.
  • Write Integration Tests for our API.

Creating REST Services Application with Spring Initializr

Spring Initializr http://start.spring.io/ is great tool to bootstrap your Spring Boot projects.
Image
As shown in the image above, following steps have to be done
  • Launch Spring Initializr and choose the following
    • Choose com.in28minutes.springboot as Group
    • Choose student-services as Artifact
    • Choose following dependencies
      • Web
      • Actuator
      • DevTools
  • Click Generate Project.
  • Import the project into Eclipse.
  • If you want to understand all the files that are part of this project, you can go here.

Implementing Business Service for your Application

All applications need data. Instead of talking to a real database, we will use an ArrayList - kind of an in-memory data store.
A student can take multiple courses. A course has an id, name, description and a list of steps you need to complete to finish the course. A student has an id, name, description and a list of courses he/she is currently registered for. We have StudentService exposing methods to
  • public List<Student> retrieveAllStudents() - Retrieve details for all students
  • public Student retrieveStudent(String studentId) - Retrieve a specific student details
  • public List<Course> retrieveCourses(String studentId) - Retrieve all courses a student is registered for
  • public Course retrieveCourse(String studentId, String courseId) - Retrieve details of a specific course a student is registered for
  • public Course addCourse(String studentId, Course course) - Add a course to an existing student
Refer to these files at the bottom of the article for exact implementation of the Service StudentService and the model classes Course and Student.
  • src/main/java/com/in28minutes/springboot/model/Course.java
  • src/main/java/com/in28minutes/springboot/model/Student.java
  • src/main/java/com/in28minutes/springboot/service/StudentService.java

Adding Couple of GET Rest Services

The Rest Service StudentController exposes couple of get services.
  • @Autowired private StudentService studentService : We are using Spring Autowiring to wire the student service into the StudentController.
  • @GetMapping("/students/{studentId}/courses"): Exposing a Get Service with studentId as a path variable
  • @GetMapping("/students/{studentId}/courses/{courseId}"): Exposing a Get Service for retrieving specific course of a student.
  • @PathVariable String studentId: Value of studentId from the uri will be mapped to this parameter.
package com.in28minutes.springboot.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import com.in28minutes.springboot.model.Course;
import com.in28minutes.springboot.service.StudentService;

@RestController
public class StudentController {

 @Autowired
 private StudentService studentService;

 @GetMapping("/students/{studentId}/courses")
 public List<Course> retrieveCoursesForStudent(@PathVariable String studentId) {
  return studentService.retrieveCourses(studentId);
 }
 
 @GetMapping("/students/{studentId}/courses/{courseId}")
 public Course retrieveDetailsForCourse(@PathVariable String studentId,
   @PathVariable String courseId) {
  return studentService.retrieveCourse(studentId, courseId);
 }

}

Executing the Get Service Using Postman

We will fire a request to http://localhost:8080/students/Student1/courses/Course1 to test the service. Response is as shown below.
{
  "id": "Course1",
  "name": "Spring",
  "description": "10 Steps",
  "steps": [
    "Learn Maven",
    "Import Project",
    "First Example",
    "Second Example"
  ]
}
Below picture shows how we can execute this Get Service from Postman - my favorite tool to run rest services. Image

Integration Testing the Get Rest Service

When we are writing an integration test for a rest service, we would want to launch the entire spring context.
  • @SpringBootTest(classes = StudentServicesApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) : Launch the entire Spring Boot Application on a Random Port
  • @LocalServerPort private int port;: Autowire the random port into the variable so that we can use it create the url.
  • createURLWithPort(String uri) : Utility method to create the url given an uri. It appends the port.
  • HttpEntity<String> entity = new HttpEntity<String>(null, headers);: We use entity so that we have the flexibility of adding in request headers in future.
  • restTemplate.exchange(createURLWithPort("/students/Student1/courses/Course1"),HttpMethod.GET, entity, String.class): Fire a GET request to the specify uri and get the response as a String.
  • JSONAssert.assertEquals(expected, response.getBody(), false) : Assert that the response contains expected fields.
package com.in28minutes.springboot.controller;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.skyscreamer.jsonassert.JSONAssert;
import org.springframework.boot.context.embedded.LocalServerPort;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;

import com.in28minutes.springboot.StudentServicesApplication;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = StudentServicesApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class StudentControllerIT {

 @LocalServerPort
 private int port;

 TestRestTemplate restTemplate = new TestRestTemplate();

 HttpHeaders headers = new HttpHeaders();

 @Test
 public void testRetrieveStudentCourse() {

  HttpEntity<String> entity = new HttpEntity<String>(null, headers);

  ResponseEntity<String> response = restTemplate.exchange(
    createURLWithPort("/students/Student1/courses/Course1"),
    HttpMethod.GET, entity, String.class);

  String expected = "{id:Course1,name:Spring,description:10 Steps}";

  JSONAssert.assertEquals(expected, response.getBody(), false);
 }

 private String createURLWithPort(String uri) {
  return "http://localhost:" + port + uri;
 }

Adding a POST Rest Service

A POST Service should return a status of created (201) when the resource creation is successful.
@PostMapping("/students/{studentId}/courses"): Mapping a url for the POST Request @RequestBody Course newCourse: Using Binding to bind the body of the request to Course object. ResponseEntity.created(location).build(): Return a status of created. Also return the location of created resource as a Response Header.
 @PostMapping("/students/{studentId}/courses")
 public ResponseEntity<Void> registerStudentForCourse(
   @PathVariable String studentId, @RequestBody Course newCourse) {

  Course course = studentService.addCourse(studentId, newCourse);

  if (course == null)
   return ResponseEntity.noContent().build();

  URI location = ServletUriComponentsBuilder.fromCurrentRequest().path(
    "/{id}").buildAndExpand(course.getId()).toUri();

  return ResponseEntity.created(location).build();
 }

Executing a POST Rest Service

Example Request is shown below. It contains all the details to register a course to a student.
{
  "name": "Microservices",
  "description": "10 Steps",
  "steps": [
    "Learn How to Break Things Up",
    "Automate the hell out of everything",
    "Have fun"
  ]
}
Below picture shows how we can execute this Post Service from Postman - my favorite tool to run rest services. Make sure you go to the Body tab and select raw. Select JSON from the dropdown. Copy above request into body.
The URL we use is http://localhost:8080/students/Student1/courses.
Image

Writing Integration Test for the POST Rest Service

When writing an integration test for a POST service we would want to check if the location header contains the uri of the created resource.
  • String actual = response.getHeaders().get(HttpHeaders.LOCATION).get(0); : Get the Location header from the response.
  • assertTrue(actual.contains("/students/Student1/courses/")) : Assert that the header contains the uri of the newly created resource.
 @Test
 public void addCourse() {

  Course course = new Course("Course1", "Spring", "10 Steps",
    Arrays.asList("Learn Maven", "Import Project", "First Example",
      "Second Example"));

  HttpEntity<Course> entity = new HttpEntity<Course>(course, headers);

  ResponseEntity<String> response = restTemplate.exchange(
    createURLWithPort("/students/Student1/courses"),
    HttpMethod.POST, entity, String.class);

  String actual = response.getHeaders().get(HttpHeaders.LOCATION).get(0);

  assertTrue(actual.contains("/students/Student1/courses/"));

 }


Next Steps

  • Understand more about Unit Testing
  • Understand more about **
  • Understand more about **
  • Understand more about **
  • Understand more about **
  • Understand more about **

Complete Code Example

pom.xml

<?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.in28minutes.springboot</groupId>
 <artifactId>student-services</artifactId>
 <version>0.0.1-SNAPSHOT</version>
 <packaging>jar</packaging>

 <name>student-services</name>
 <description>Demo project for Spring Boot</description>

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

 <properties>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  <java.version>1.8</java.version>
 </properties>

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

  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-devtools</artifactId>
   <scope>runtime</scope>
  </dependency>
  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <scope>test</scope>
  </dependency>
 </dependencies>

 <build>
  <plugins>
   <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
   </plugin>
  </plugins>
 </build>


</project>

src/main/java/com/in28minutes/springboot/controller/StudentController.java

package com.in28minutes.springboot.controller;

import java.net.URI;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import com.in28minutes.springboot.model.Course;
import com.in28minutes.springboot.service.StudentService;

@RestController
public class StudentController {

 @Autowired
 private StudentService studentService;

 @GetMapping("/students/{studentId}/courses")
 public List<Course> retrieveCoursesForStudent(@PathVariable String studentId) {
  return studentService.retrieveCourses(studentId);
 }
 
 @GetMapping("/students/{studentId}/courses/{courseId}")
 public Course retrieveDetailsForCourse(@PathVariable String studentId,
   @PathVariable String courseId) {
  return studentService.retrieveCourse(studentId, courseId);
 }
 
 @PostMapping("/students/{studentId}/courses")
 public ResponseEntity<Void> registerStudentForCourse(
   @PathVariable String studentId, @RequestBody Course newCourse) {

  Course course = studentService.addCourse(studentId, newCourse);

  if (course == null)
   return ResponseEntity.noContent().build();

  URI location = ServletUriComponentsBuilder.fromCurrentRequest().path(
    "/{id}").buildAndExpand(course.getId()).toUri();

  return ResponseEntity.created(location).build();
 }

}

src/main/java/com/in28minutes/springboot/model/Course.java

package com.in28minutes.springboot.model;

import java.util.List;

public class Course {
 private String id;
 private String name;
 private String description;
 private List<String> steps;

 // Needed by Caused by: com.fasterxml.jackson.databind.JsonMappingException:
 // Can not construct instance of com.in28minutes.springboot.model.Course:
 // no suitable constructor found, can not deserialize from Object value
 // (missing default constructor or creator, or perhaps need to add/enable
 // type information?)
 public Course() {

 }

 public Course(String id, String name, String description, List<String> steps) {
  super();
  this.id = id;
  this.name = name;
  this.description = description;
  this.steps = steps;
 }

 public String getId() {
  return id;
 }

 public void setId(String id) {
  this.id = id;
 }

 public String getDescription() {
  return description;
 }

 public String getName() {
  return name;
 }

 public List<String> getSteps() {
  return steps;
 }

 @Override
 public String toString() {
  return String.format(
    "Course [id=%s, name=%s, description=%s, steps=%s]", id, name,
    description, steps);
 }

 @Override
 public int hashCode() {
  final int prime = 31;
  int result = 1;
  result = prime * result + ((id == null) ? 0 : id.hashCode());
  return result;
 }

 @Override
 public boolean equals(Object obj) {
  if (this == obj)
   return true;
  if (obj == null)
   return false;
  if (getClass() != obj.getClass())
   return false;
  Course other = (Course) obj;
  if (id == null) {
   if (other.id != null)
    return false;
  } else if (!id.equals(other.id))
   return false;
  return true;
 }

}

src/main/java/com/in28minutes/springboot/model/Student.java

package com.in28minutes.springboot.model;

import java.util.List;

public class Student {
 private String id;
 private String name;
 private String description;
 private List<Course> courses;

 public Student(String id, String name, String description,
   List<Course> courses) {
  super();
  this.id = id;
  this.name = name;
  this.description = description;
  this.courses = courses;
 }

 public String getId() {
  return id;
 }

 public void setId(String id) {
  this.id = id;
 }

 public String getName() {
  return name;
 }

 public void setName(String name) {
  this.name = name;
 }

 public String getDescription() {
  return description;
 }

 public void setDescription(String description) {
  this.description = description;
 }

 public List<Course> getCourses() {
  return courses;
 }

 public void setCourses(List<Course> courses) {
  this.courses = courses;
 }

 @Override
 public String toString() {
  return String.format(
    "Student [id=%s, name=%s, description=%s, courses=%s]", id,
    name, description, courses);
 }
}

src/main/java/com/in28minutes/springboot/service/StudentService.java

package com.in28minutes.springboot.service;

import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.springframework.stereotype.Component;

import com.in28minutes.springboot.model.Course;
import com.in28minutes.springboot.model.Student;

@Component
public class StudentService {

 private static List<Student> students = new ArrayList<>();

 static {
  //Initialize Data
  Course course1 = new Course("Course1", "Spring", "10 Steps", Arrays
    .asList("Learn Maven", "Import Project", "First Example",
      "Second Example"));
  Course course2 = new Course("Course2", "Spring MVC", "10 Examples",
    Arrays.asList("Learn Maven", "Import Project", "First Example",
      "Second Example"));
  Course course3 = new Course("Course3", "Spring Boot", "6K Students",
    Arrays.asList("Learn Maven", "Learn Spring",
      "Learn Spring MVC", "First Example", "Second Example"));
  Course course4 = new Course("Course4", "Maven",
    "Most popular maven course on internet!", Arrays.asList(
      "Pom.xml", "Build Life Cycle", "Parent POM",
      "Importing into Eclipse"));

  Student ranga = new Student("Student1", "Ranga Karanam",
    "Hiker, Programmer and Architect", new ArrayList<>(Arrays
      .asList(course1, course2, course3, course4)));

  Student satish = new Student("Student2", "Satish T",
    "Hiker, Programmer and Architect", new ArrayList<>(Arrays
      .asList(course1, course2, course3, course4)));

  students.add(ranga);
  students.add(satish);
 }

 public List<Student> retrieveAllStudents() {
  return students;
 }

 public Student retrieveStudent(String studentId) {
  for (Student student : students) {
   if (student.getId().equals(studentId)) {
    return student;
   }
  }
  return null;
 }

 public List<Course> retrieveCourses(String studentId) {
  Student student = retrieveStudent(studentId);

  if (student == null) {
   return null;
  }

  return student.getCourses();
 }

 public Course retrieveCourse(String studentId, String courseId) {
  Student student = retrieveStudent(studentId);

  if (student == null) {
   return null;
  }

  for (Course course : student.getCourses()) {
   if (course.getId().equals(courseId)) {
    return course;
   }
  }

  return null;
 }

 private SecureRandom random = new SecureRandom();

 public Course addCourse(String studentId, Course course) {
  Student student = retrieveStudent(studentId);

  if (student == null) {
   return null;
  }

  String randomId = new BigInteger(130, random).toString(32);
  course.setId(randomId);

  student.getCourses().add(course);

  return course;
 }
}

src/main/java/com/in28minutes/springboot/StudentServicesApplication.java

package com.in28minutes.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class StudentServicesApplication {

 public static void main(String[] args) {
  SpringApplication.run(StudentServicesApplication.class, args);
 }
}

src/main/resources/application.properties


src/test/java/com/in28minutes/springboot/controller/StudentControllerIT.java

package com.in28minutes.springboot.controller;

import static org.junit.Assert.assertTrue;

import java.util.Arrays;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.skyscreamer.jsonassert.JSONAssert;
import org.springframework.boot.context.embedded.LocalServerPort;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;

import com.in28minutes.springboot.StudentServicesApplication;
import com.in28minutes.springboot.model.Course;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = StudentServicesApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class StudentControllerIT {

 @LocalServerPort
 private int port;

 TestRestTemplate restTemplate = new TestRestTemplate();

 HttpHeaders headers = new HttpHeaders();

 @Test
 public void testRetrieveStudentCourse() {

  HttpEntity<String> entity = new HttpEntity<String>(null, headers);

  ResponseEntity<String> response = restTemplate.exchange(
    createURLWithPort("/students/Student1/courses/Course1"),
    HttpMethod.GET, entity, String.class);

  String expected = "{id:Course1,name:Spring,description:10 Steps}";

  JSONAssert.assertEquals(expected, response.getBody(), false);
 }

 @Test
 public void addCourse() {

  Course course = new Course("Course1", "Spring", "10 Steps",
    Arrays.asList("Learn Maven", "Import Project", "First Example",
      "Second Example"));

  HttpEntity<Course> entity = new HttpEntity<Course>(course, headers);

  ResponseEntity<String> response = restTemplate.exchange(
    createURLWithPort("/students/Student1/courses"),
    HttpMethod.POST, entity, String.class);

  String actual = response.getHeaders().get(HttpHeaders.LOCATION).get(0);

  assertTrue(actual.contains("/students/Student1/courses/"));

 }

 private String createURLWithPort(String uri) {
  return "http://localhost:" + port + uri;
 }

}

0 comments:

Post a Comment