Improving Spring Mock-MVC Tests

Spring Mock- MVC  provides excellent testing methods for Spring Boot REST APIs. Mock-MVC allows us to test Spring-MVC request handling without starting a real server.

I've used Mock-MVC tests on various projects and in my experience they are often quite verbose. This isn't necessarily a bad thing. However, this often requires copying / pasting similar code fragments into test classes. In this post, we'll take a look at several ways to improve Spring Mock-MVC tests.

Decide what to test with Mock-MVC

The first question we need to ask is what we want to test with Mock-MVC. Here are some examples of test cases:

  • Testing only the web layer and emulating all controller dependencies.

  • Testing the web tier with domain logic and simulating third-party dependencies such as databases or message queues.

  • Testing the full path from the web layer to the database by replacing third-party dependencies with embedded alternatives if possible (e.g.  H2  or  embedded-Kafka  )

All of these scenarios have their pros and cons. However, I think there are two simple rules that we must follow:

  • Test as much as possible in standard JUnit tests (no Spring). This greatly improves test performance and makes it easier to write tests.

  • (-), Spring, , . . Spring , .

JUnit . , , Mock-MVC, , , .

Spring Spring .

, @MockMvcTest:

@SpringBootTest
@TestPropertySource(locations = "classpath:test.properties")
@AutoConfigureMockMvc(secure = false)
@Retention(RetentionPolicy.RUNTIME)
public @interface MockMvcTest {}

:

@MockMvcTest
public class MyTest {
    ...
}

, .  Spring .

Mock-MVC

Mock-MVC , :

mockMvc.perform(put("/products/42")
        .contentType(MediaType.APPLICATION_JSON)
        .accept(MediaType.APPLICATION_JSON)
        .content("{\"name\": \"Cool Gadget\", \"description\": \"Looks cool\"}")
        .header("Authorization", getBasicAuthHeader("John", "secr3t")))
        .andExpect(status().isOk());

PUT JSON /products/42.

, - JSON Java. , , , , Java, .

, JSON.  , .  Java Text  JDK 13/14 .  - , .

JSON . :

mvc.perform(put("/products/42")
        .contentType(MediaType.APPLICATION_JSON)
        .accept(MediaType.APPLICATION_JSON)
        .content("""
            {
                "name": "Cool Gadget",
                "description": "Looks cool"
            }
            """)
        .header("Authorization", getBasicAuthHeader("John", "secr3t")))
        .andExpect(status().isOk()); 

.

, -, , JSON , JSON.

:

Product product = new Product("Cool Gadget", "Looks cool");
mvc.perform(put("/products/42")
        .contentType(MediaType.APPLICATION_JSON)
        .accept(MediaType.APPLICATION_JSON)
        .content(objectToJson(product))
        .header("Authorization", getBasicAuthHeader("John", "secr3t")))
        .andExpect(status().isOk());

product JSON    objectToJson(..).  .  , .

, .  JSON REST-API, , PUT.  :

public static MockHttpServletRequestBuilder putJson(String uri, Object body) {
    try {
        String json = new ObjectMapper().writeValueAsString(body);
        return put(uri)
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON)
                .content(json);
    } catch (JsonProcessingException e) {
        throw new RuntimeException(e);
    }
}

  body  JSON Jackson ObjectMapper .  PUT   Accept  Content-Type .

:

Product product = new Product("Cool Gadget", "Looks cool");
mvc.perform(putJson("/products/42", product)
        .header("Authorization", getBasicAuthHeader("John", "secr3t")))
        .andExpect(status().isOk())

, .   putJson(..)  MockHttpServletRequestBuilder.  , , (,    ).

- , Spring Mock-MVC.    putJson(..).  PUT , , -.

RequestPostProcessor  .  , RequestPostProcessor  .  .

:

public static RequestPostProcessor authentication() {
    return request -> {
        request.addHeader("Authorization", getBasicAuthHeader("John", "secr3t"));
        return request;
    };
} 

 authentication()  RequestPostProcessor,  .   RequestPostProcessor   with(..):

Product product = new Product("Cool Gadget", "Looks cool");
mvc.perform(putJson("/products/42", product).with(authentication()))
        .andExpect(status().isOk())

.  , , .  , putJson(url, data).with(authentication())  .

, .

:

mvc.perform(get("/products/42"))
        .andExpect(status().isOk())
        .andExpect(header().string("Cache-Control", "no-cache"))
        .andExpect(jsonPath("$.name").value("Cool Gadget"))
        .andExpect(jsonPath("$.description").value("Looks cool"));

HTTP, ,   Cache-Control   no-cache,  JSON-Path .

 Cache-Control  , ,  ,  .  :

public ResultMatcher noCacheHeader() {
    return header().string("Cache-Control", "no-cache");
}

,  noCacheHeader()  andExpect(..):

mvc.perform(get("/products/42"))
        .andExpect(status().isOk())
        .andExpect(noCacheHeader())
        .andExpect(jsonPath("$.name").value("Cool Gadget"))
        .andExpect(jsonPath("$.description").value("Looks cool"));

.

,   product(..),  JSON   Product:

public static ResultMatcher product(String prefix, Product product) {
    return ResultMatcher.matchAll(
            jsonPath(prefix + ".name").value(product.getName()),
            jsonPath(prefix + ".description").value(product.getDescription())
    );
}

:

Product product = new Product("Cool Gadget", "Looks cool");
mvc.perform(get("/products/42"))
        .andExpect(status().isOk())
        .andExpect(noCacheHeader())
        .andExpect(product("$", product));

,   prefix  . , , JSON .

, .    prefix  . :

Product product0 = ..
Product product1 = ..
mvc.perform(get("/products"))
        .andExpect(status().isOk())
        .andExpect(product("$[0]", product0))
        .andExpect(product("$[1]", product1));

   ResultMatcher  .  .

Spring Mock-MVC.  Mock-MVC, , .  ( Spring Mock-MVC).

Spring Mock-MVC.   RequestPostProcessor  .  ResultMatcher  .

You can find the code examples on  GitHub .




All Articles