Spring Boot测试中使用REST Assured(转)

 

原文:https://rieckpil.de/testing-spring-boot-applications-with-rest-assured/

REST Assured is a Java DSL (Domain Specific Langauge) that aims to simplify testing REST APIs. It follows a BDD (Behavior Driven Development) approach and is influenced by testing APIs with dynamic languages like Groovy. We can use REST Assured to test the REST API of any Java project as it is framework independent. However, REST Assured comes with a great Spring integration for testing our @RestController endpoints that we're about to explore with this article.

Spring Boot and REST Assured Project Setup

For our demo application we use Java 11, Spring Boot 2.4.0, and the following dependencies:

 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 <?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">   <modelVersion>4.0.0</modelVersion>     <parent>     <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter-parent</artifactId>     <version>2.4.0</version>     <relativePath/> <!-- lookup parent from repository -->   </parent>     <groupId>de.rieckpil.blog</groupId>   <artifactId>spring-boot-rest-assured</artifactId>   <version>0.0.1-SNAPSHOT</version>   <name>spring-boot-rest-assured</name>     <properties>     <java.version>11</java.version>     <groovy.version>3.0.7</groovy.version>     <rest-assured.version>4.3.3</rest-assured.version>   </properties>     <dependencies>       <!-- Spring Boot Starters for Web, Validation, and Security -->       <dependency>       <groupId>io.rest-assured</groupId>       <artifactId>spring-mock-mvc</artifactId>       <scope>test</scope>     </dependency>       <dependency>       <groupId>org.springframework.boot</groupId>       <artifactId>spring-boot-starter-test</artifactId>       <scope>test</scope>     </dependency>     <dependency>       <groupId>org.springframework.security</groupId>       <artifactId>spring-security-test</artifactId>       <scope>test</scope>     </dependency>   </dependencies>     <build>     <!-- Spring Boot Maven Plugin -->   </build>   </project>

As Spring Boot manages the dependency version of REST Assured and all its modules, we can define the spring-mock-mvc without an explicit version. We can override the managed version consistently with the rest-assured.version property.

If we mix up our dependency versions for REST Assured and try to outsmart Spring Boot, we might see several exceptions during test runtime:

 
1 2 3 4 5 java.lang.NoClassDefFoundError: io/restassured/internal/common/assertion/AssertParameter       at io.restassured.module.mockmvc.config.MockMvcParamConfig.<init>(MockMvcParamConfig.java:65)     at io.restassured.module.mockmvc.config.MockMvcParamConfig.<init>(MockMvcParamConfig.java:43)     at io.restassured.module.mockmvc.config.RestAssuredMockMvcConfig.<init>(RestAssuredMockMvcConfig.java:56)

As REST Assured has quite some transitive dependencies (Groovy related dependencies, Apache HttpClient, Hamcrest, and Tagsoup), we should favor the manged version from our Spring Boot release.

As I've seen several integration issues for REST Assured and Java 11 projects, consider the following tips:

  • IntelliJ IDEA can help to identify duplicated dependency includes for a Maven project. We can open a visual dependency graph with the following steps: right-click inside our pom.xml -> Maven -> Show Dependencies. A red arrow indicates a violation for dependency convergence. The Maven Enforcer plugin can even detect this as part of your build.
  • REST Assured also provides a rest-assured-all dependency, that can help to solve split package problems.
  • When using Spring Boot together with REST Assured, you might also stumble over exceptions for Groovy related parts of this testing library. Explicitly overriding Spring Boot's default Groovy version with groovy.version inside the pom.xml seems to help here.

The Spring MVC RestController Under Test

Our sample application is a book store that exposes the following REST API endpoints to manage its inventory:

 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @RestController @RequestMapping("/api/books") public class BookController {     private final BookService bookService;     public BookController(BookService bookService) {     this.bookService = bookService;   }     @GetMapping   public List<Book> getAllBooks(@RequestParam(value = "amount", defaultValue = "500") int amount) {     return bookService.getAllBooks(amount);   }     @GetMapping("/{id}")   public ResponseEntity<Book> getBookById(@PathVariable("id") Long id) {     return ResponseEntity.ok(bookService.getBookById(id));   }     @PostMapping   public ResponseEntity<Void> createNewBook(     @Valid @RequestBody BookRequest bookRequest,     UriComponentsBuilder uriComponentsBuilder) {       Long bookId = bookService.createNewBook(bookRequest);       UriComponents uriComponents = uriComponentsBuilder.path("/api/books/{id}").buildAndExpand(bookId);     HttpHeaders headers = new HttpHeaders();     headers.setLocation(uriComponents.toUri());       return new ResponseEntity<>(headers, HttpStatus.CREATED);   } }

The actual implementation of the BookService doesn't matter for this demonstration and we can assume it stores our book entities somewhere (e.g. database or in-memory).

For a more realistic example, we'll secure the HTTP POST endpoint and only allow authenticated users that have the ADMIN role to access it:

  Spring Boot测试中使用REST Assured(转)

JUnit 5 & Mockito Cheat Sheet

Answering 24 questions for the two most essential Java testing libraries.

 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter {     @Override   protected void configure(HttpSecurity http) throws Exception {     http       .csrf()       .disable()       .httpBasic()       .and()       .authorizeRequests(         requests -> requests           .mvcMatchers(HttpMethod.GET, "/api/books").permitAll()           .mvcMatchers(HttpMethod.GET, "/api/books/*").permitAll()           .mvcMatchers(HttpMethod.POST, "/api/books").hasRole("ADMIN")           .anyRequest().authenticated());   } }

 

REST Assured Powered MockMvc Test

There are multiple ways to configure REST Assured for a Spring MVC test case. We can manually pass a list of controller objects, provide a MockMvcBuilder , pass a WebApplicationContext or a MockMvc instance.

As we can inject an already initialized MockMvc bean to our test when using Spring Boot's test slice annotation @WebMvcTest, we'll use it for our REST Assured configuration.

 
1 2 3 4 @BeforeEach void setUp() {   RestAssuredMockMvc.mockMvc(mockMvc); }

All upcoming requests for this test class will target this MockMvc instance unless we override it explicitly.

With this setup in place, we can write our first test that ensures unauthenticated clients can request our book store's inventory:

 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Test void shouldAllowBookRetrievalWithoutAuthentication() {     Mockito.when(bookService.getAllBooks(42)).thenReturn(     List.of(new Book(42L, "42", "REST Assured With Spring Boot", "Duke")));     RestAssuredMockMvc     .given()       .auth().none()       .param("amount", 42)     .when()       .get("/api/books")     .then()       .statusCode(200)       .body("$.size()", Matchers.equalTo(1))       .body("[0].id", Matchers.equalTo(42))       .body("[0].isbn", Matchers.equalTo("42"))       .body("[0].author", Matchers.equalTo("Duke"))       .body("[0].title", Matchers.equalTo("REST Assured With Spring Boot")); }

Due to REST Assured's fluent API, one should easily grasp the test setup above. REST Assured also follows a BDD style and  .given().when().then() gives each request a standardized schema.

For writing expectations for our HTTP response body/status/header, we then use Matchers from Hamcrest.

The test above doesn't use static imports because there is a small pitfall. When importing the .given() method from REST Assured, we have to make sure it's not the traditional non-Spring variant:

 
1 2 3 4 5 6 // import this when using static imports import static io.restassured.module.mockmvc.RestAssuredMockMvc.given;     // NOT this import static io.restassured.RestAssured.given;

Furthermore, there is a potential second pitfall when using a static import and Mockito in the same test.

For tests where we don't need a .given() step (e.g., we don't pass any query parameter/path variable/header), we can omit it and start our request specification with .when() :

 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Test void shouldAllowBookRetrievalWithoutAuthenticationShort() {     Mockito.when(bookService.getAllBooks(42)).thenReturn(     List.of(new Book(42L, "42", "REST Assured With Spring Boot", "Duke")));     RestAssuredMockMvc     .when()       .get("/api/books")     .then()       .statusCode(200)       .body("$.size()", Matchers.equalTo(1))       .body("[0].id", Matchers.equalTo(42))       .body("[0].isbn", Matchers.equalTo("42"))       .body("[0].author", Matchers.equalTo("Duke"))       .body("[0].title", Matchers.equalTo("REST Assured With Spring Boot")); }

As soon as we add a static import for REST Assured's .when() we would * with Mockito's .when() unless we are more explicit and use Mockito.when(). This is something to keep in mind.

Testing a Protected Endpoint

Most tutorials end here as they showed how to test only one Hello World API endpoint. But that's not what we aim for. Let's take it to the next step and write a test for our protected HTTP POST endpoint.

REST Assured already comes with great support for several authentication mechanisms. Besides that, we can integrate the SecurityMockMvcRequestPostProcessors from the spring-security-test dependency that we might be already familiar with from other MockMvc tests.

 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Test void shouldAllowBookCreationForAuthenticatedAdminUsers() {     Mockito.when(bookService.createNewBook(any(BookRequest.class))).thenReturn(42L);     RestAssuredMockMvc     .given()       .auth().with(SecurityMockMvcRequestPostProcessors.user("duke").roles("ADMIN"))       .contentType("application/json")       .body("{\"title\": \"Effective Java\", \"isbn\":\"978-0-13-468599-1 \", \"author\":\"Joshua Bloch\"}")     .when()       .post("/api/books")     .then()       .statusCode(201)       .header("Location", Matchers.containsString("/api/books/42")); }

As part of the .given() step, we define the HTTP request body, set the content type, and associate a user. The Spring Security Test RequestPostProcessor functionality gives us full control over the user setup (username, password, roles, authorities) for this test.

We can also use the @WithMockUser annotation for our tests:

 
1 2 3 4 5 6 7 8 9 10 11 12 13 @Test @WithMockUser(username = "duke", roles = {"USER", "EDITOR"}) void shouldBlockBookCreationForNonAdminUsers() {     RestAssuredMockMvc     .given()       .contentType("application/json")       .body("{\"title\": \"Effective Java\", \"isbn\":\"978-0-13-468599-1 \", \"author\":\"Joshua Bloch\"}")     .when()       .post("/api/books")     .then()       .statusCode(403); }

 

Further Tips For REST Assured with Spring Boot

For Spring WebFlux controller endpoints that use the WebTestClient for tests, REST Assured provides a spring-web-test-client module. There are also extensions and support for both Scala and Kotlin available.

While REST Assured is designed to verify and test REST APIs, we can also test @Controller endpoints with the spring-mock-mvc module.

Let's assume one of our controller endpoints returns a Thymeleaf view and populates its Model. As we can add any ResultMatcher to our .then() verification part, we can write the following expectation:

 
1 2 3 4 .then()   .statusCode(200)   .expect(MockMvcResultMatchers.model().attributeExists("analyticsData"))   .expect(MockMvcResultMatchers.view().name("dashboard"));

However, REST Assured won't provide any additional syntactic sugar or helper methods to verify @Controller endpoints and we just use Spring's MockMvcResultMatchers.

We can also use REST Assured when writing integration tests for our Spring Boot application. Let's verify that we can create a book and a subsequent query for the book returns it:

 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 @SpringBootTest(   webEnvironment = WebEnvironment.RANDOM_PORT,   properties = {   "spring.security.user.name=duke",   "spring.security.user.password=secret",   "spring.security.user.roles=ADMIN" }) class ApplicationTest {     @LocalServerPort   private Integer port;     @Test   void shouldCreateBook() {       ExtractableResponse<Response> response = RestAssured       .given()         .filter(new RequestLoggingFilter())         .auth().preemptive().basic("duke", "secret")         .contentType("application/json")         .body("{\"title\": \"Effective Java\", \"isbn\":\"978-0-13-468599-1\", \"author\":\"Joshua Bloch\"}")       .when()         .post("http://localhost:" + port + "/api/books")       .then()         .statusCode(201)         .extract();       RestAssured       .when()         .get(response.header("Location"))       .then()         .statusCode(200)         .body("id", Matchers.notNullValue())         .body("isbn", Matchers.equalTo("978-0-13-468599-1"))         .body("author", Matchers.equalTo("Joshua Bloch"))         .body("title", Matchers.equalTo("Effective Java"));   } }

Keep in mind that we don't use MockMvc and access our Spring Boot application on a local port for such an integration test. That's why we have to replace RestAssuredMockMvc.given() with RestAssured.given().

Summary of using REST Assured with Spring Boot

Due to the feature-rich and convenient API of MockMvc and Spring Boot's excellent test support, I wouldn't consider REST Assured a must-have. The spring-mock-mvc module is basically a wrapper on top of MockMvc that brings REST Assured's fluent API to it. However, REST Assured still offers great features on top:

If your team is already familiar with REST Assured from testing REST APIs of other projects (e.g. non-Spring Boot), it's surely a good fit for your Spring Boot application. Once resolve potential incompatibility issues and are aware of the pitfalls, REST Assured can be a great add-on for your testing toolbox.

All source code examples for this REST Assured with Spring Boot demo are available on GitHub.

Have fun using REST Assured with Spring Boot,

Philip

上一篇:分布式场景下防重点实现思路(后端)


下一篇:接口自动化之配置rest-assured全局地址