Integration Testing in Spring – 在Spring中进行集成测试

最后修改: 2016年 8月 18日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

Integration testing plays an important role in the application development cycle by verifying the end-to-end behavior of a system.

集成测试通过验证系统的端到端行为,在应用开发周期中发挥了重要作用。

In this tutorial, we’ll learn how to leverage the Spring MVC test framework in order to write and run integration tests that test controllers without explicitly starting a Servlet container.

在本教程中,我们将学习如何利用Spring MVC测试框架来编写和运行测试控制器的集成测试,而无需明确启动Servlet容器。

2. Preparation

2.准备

We’ll need several Maven dependencies for running the integration tests we’ll use in this article. First and foremost, we’ll need the latest junit-jupiter-engine, junit-jupiter-api, and Spring test dependencies:

我们需要几个Maven依赖项来运行本文要用到的集成测试。首先,我们需要最新的junit-jupiter-enginejunit-jupiter-apiSpring test依赖项。

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.8.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.8.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.3.3</version>
    <scope>test</scope>
</dependency>

For effective asserting of results, we’ll also use Hamcrest and JSON path:

为了有效地断言结果,我们还将使用HamcrestJSON路径

<dependency>
    <groupId>org.hamcrest</groupId>
    <artifactId>hamcrest-library</artifactId>
    <version>2.2</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>com.jayway.jsonpath</groupId>
    <artifactId>json-path</artifactId>
    <version>2.5.0</version>
    <scope>test</scope>
</dependency>

3. Spring MVC Test Configuration

3.Spring MVC测试配置

Now let’s see how to configure and run the Spring enabled tests.

现在让我们看看如何配置和运行Spring启用的测试。

3.1. Enable Spring in Tests With JUnit 5

3.1.用JUnit 5在测试中启用Spring

JUnit 5 defines an extension interface through which classes can integrate with the JUnit test.

JUnit 5定义了一个扩展接口,通过它,类可以与JUnit测试集成。

We can enable this extension by adding the @ExtendWith annotation to our test classes and specifying the extension class to load. To run the Spring test, we use SpringExtension.class.

我们可以通过在测试类中添加@ExtendWith注解并指定要加载的扩展类来启用这个扩展。为了运行Spring测试,我们使用SpringExtension.class.

We’ll also need the @ContextConfiguration annotation to load the context configuration and bootstrap the context that our test will use.

我们还需要@ContextConfiguration注解来加载上下文配置和引导我们测试将使用的上下文

Let’s have a look:

让我们看一看。

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = { ApplicationConfig.class })
@WebAppConfiguration
public class GreetControllerIntegrationTest {
    ....
}

Notice that in @ContextConfiguration, we provided the ApplicationConfig.class config class, which loads the configuration we need for this particular test.

注意在@ContextConfiguration,中我们提供了ApplicationConfig.class配置类,它加载了我们在这个特定测试中需要的配置。

We’ll use a Java configuration class here to specify the context configuration. Similarly, we can use the XML-based configuration:

我们将在这里使用一个Java配置类来指定上下文配置。同样地,我们也可以使用基于XML的配置。

@ContextConfiguration(locations={""})

Finally, we’ll also annotate the test with @WebAppConfiguration, which will load the web application context.

最后,我们还将用@WebAppConfiguration对测试进行注解,这将加载Web应用程序上下文

By default, it looks for the root web application at path src/main/webapp. We can override this location by simply passing the value attribute:

默认情况下,它在路径src/main/webapp.中寻找根Web应用程序,我们可以通过简单地传递value属性来覆盖这个位置。

@WebAppConfiguration(value = "")

3.2. The WebApplicationContext Object

3.2.WebApplicationContext 对象

WebApplicationContext provides a web application configuration. It loads all the application beans and controllers into the context.

WebApplicationContext提供了一个Web应用配置。它将所有的应用Bean和控制器加载到上下文中。

Now we’ll be able to wire the web application context right into the test:

现在我们就可以把网络应用的上下文直接接入测试。

@Autowired
private WebApplicationContext webApplicationContext;

3.3. Mocking Web Context Beans

3.3.嘲弄Web上下文Bean

MockMvc provides support for Spring MVC testing. It encapsulates all web application beans and makes them available for testing.

MockMvc为Spring MVC测试提供支持。它封装了所有的Web应用Bean,并使它们可用于测试。

Let’s see how to use it:

让我们看看如何使用它。

private MockMvc mockMvc;
@BeforeEach
public void setup() throws Exception {
    this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext).build();
}

We’ll initialize the mockMvc object in the @BeforeEach annotated method, so that we don’t have to initialize it inside every test.

我们将在@BeforeEach注解的方法中初始化mockMvc对象,这样我们就不必在每个测试中初始化它。

3.4. Verify Test Configuration

3.4.验证测试配置

Let’s verify that we’re loading the WebApplicationContext object (webApplicationContext) properly. We’ll also check that the right servletContext is being attached:

让我们验证一下我们是否正确地加载了WebApplicationContext对象(webApplicationContext)。我们还将检查正确的servletContext是否被附加。

@Test
public void givenWac_whenServletContext_thenItProvidesGreetController() {
    ServletContext servletContext = webApplicationContext.getServletContext();
    
    Assert.assertNotNull(servletContext);
    Assert.assertTrue(servletContext instanceof MockServletContext);
    Assert.assertNotNull(webApplicationContext.getBean("greetController"));
}

Notice that we’re also checking that a GreetController.java bean exists in the web context. This ensures that Spring beans are loaded properly. At this point, the setup of the integration test is done. Now, we’ll see how we can test resource methods using the MockMvc object.

请注意,我们也在检查Web上下文中是否存在一个GreetController.javabean。这可以确保Spring Bean被正确加载。在这一点上,集成测试的设置已经完成。现在,我们将看到如何使用MockMvc对象测试资源方法。

4. Writing Integration Tests

4.编写集成测试

In this section, we’ll go over the basic operations available through the test framework.

在这一节中,我们将介绍通过测试框架可用的基本操作。

We’ll look at how to send requests with path variables and parameters. We’ll also follow with a few examples that show how to assert that the proper view name is resolved, or that the response body is as expected.

我们将研究如何发送带有路径变量和参数的请求。我们还将通过一些例子来说明如何断言正确的视图名称得到了解决,或者断言响应体符合预期。

The snippets that are shown below use static imports from the MockMvcRequestBuilders or MockMvcResultMatchers classes.

下面显示的片段使用了从MockMvcRequestBuildersMockMvcResultMatchers类的静态导入。

4.1. Verify View Name

4.1.验证视图名称

We can invoke the /homePage endpoint from our test as:

我们可以从我们的测试中调用/homePage端点作为:

http://localhost:8080/spring-mvc-test/

or

http://localhost:8080/spring-mvc-test/homePage

First, let’s see the test code:

首先,让我们看看测试代码。

@Test
public void givenHomePageURI_whenMockMVC_thenReturnsIndexJSPViewName() {
    this.mockMvc.perform(get("/homePage")).andDo(print())
      .andExpect(view().name("index"));
}

Let’s break it down:

让我们把它分解一下。

  • perform() method will call a GET request method, which returns the ResultActions. Using this result, we can have assertion expectations about the response, like its content, HTTP status, or header.
  • andDo(print()) will print the request and response. This is helpful to get a detailed view in case of an error.
  • andExpect() will expect the provided argument. In our case, we’re expecting “index” to be returned via MockMvcResultMatchers.view().

4.2. Verify Response Body

4.2.验证响应主体

We’ll invoke the /greet endpoint from our test as:

我们将从我们的测试中调用/greet端点作为。

http://localhost:8080/spring-mvc-test/greet

The expected output will be:

预期产出将是。

{
    "id": 1,
    "message": "Hello World!!!"
}

Let’s see the test code:

让我们看看测试代码。

@Test
public void givenGreetURI_whenMockMVC_thenVerifyResponse() {
    MvcResult mvcResult = this.mockMvc.perform(get("/greet"))
      .andDo(print()).andExpect(status().isOk())
      .andExpect(jsonPath("$.message").value("Hello World!!!"))
      .andReturn();
    
    Assert.assertEquals("application/json;charset=UTF-8", 
      mvcResult.getResponse().getContentType());
}

Let’s see exactly what’s going on:

让我们看看到底发生了什么事。

  • andExpect(MockMvcResultMatchers.status().isOk()) will verify that response HTTP status is Ok (200). This ensures that the request was successfully executed.
  • andExpect(MockMvcResultMatchers.jsonPath(“$.message”).value(“Hello World!!!”)) will verify that the response content matches with the argument “Hello World!!!” Here, we used jsonPath, which extracts the response content and provides the requested value.
  • andReturn() will return the MvcResult object, which is used when we have to verify something that isn’t directly achievable by the library. In this case, we’ve added assertEquals to match the content type of the response that is extracted from the MvcResult object.

4.3. Send GET Request With Path Variable

4.3.用路径变量发送GET请求

We’ll invoke the /greetWithPathVariable/{name} endpoint from our test as:

我们将从我们的测试中调用/greetWithPathVariable/{name}端点作为。

http://localhost:8080/spring-mvc-test/greetWithPathVariable/John

The expected output will be:

预期产出将是。

{
    "id": 1,
    "message": "Hello World John!!!"
}

Let’s see the test code:

让我们看看测试代码。

@Test
public void givenGreetURIWithPathVariable_whenMockMVC_thenResponseOK() {
    this.mockMvc
      .perform(get("/greetWithPathVariable/{name}", "John"))
      .andDo(print()).andExpect(status().isOk())
      
      .andExpect(content().contentType("application/json;charset=UTF-8"))
      .andExpect(jsonPath("$.message").value("Hello World John!!!"));
}

MockMvcRequestBuilders.get(“/greetWithPathVariable/{name}”, “John”) will send a request as “/greetWithPathVariable/John.

MockMvcRequestBuilders.get(“/greetWithPathVariable/{name}”, “John”)将发送一个请求为”/greetWithPathVariable/John.

This becomes easier with respect to readability and knowing what parameters are dynamically set in the URL. Note that we can pass as many path parameters as needed.

这在可读性和了解URL中动态设置的参数方面变得更容易。请注意,我们可以根据需要传递尽可能多的路径参数。

4.4. Send GET Request With Query Parameters

4.4.发送带有查询参数的GET请求

We’ll invoke the /greetWithQueryVariable?name={name} endpoint from our test as:

我们将从我们的测试中调用/greetWithQueryVariable?name={name}端点作为。

http://localhost:8080/spring-mvc-test/greetWithQueryVariable?name=John%20Doe

In this case, the expected output will be:

在这种情况下,预期输出将是。

{
    "id": 1,
    "message": "Hello World John Doe!!!"
}

Now, let’s see the test code:

现在,让我们看看测试代码。

@Test
public void givenGreetURIWithQueryParameter_whenMockMVC_thenResponseOK() {
    this.mockMvc.perform(get("/greetWithQueryVariable")
      .param("name", "John Doe")).andDo(print()).andExpect(status().isOk())
      .andExpect(content().contentType("application/json;charset=UTF-8"))
      .andExpect(jsonPath("$.message").value("Hello World John Doe!!!"));
}

param(“name”, “John Doe”) will append the query parameter in the GET request. This is similar to “/greetWithQueryVariable?name=John%20Doe.

param(“name”, “John Doe”)将在GET请求中附加查询参数。这类似于”/greetWithQueryVariable?name=John%20Doe.

The query parameter can also be implemented using the URI template style:

查询参数也可以使用URI模板风格来实现。

this.mockMvc.perform(
  get("/greetWithQueryVariable?name={name}", "John Doe"));

4.5. Send POST Request

4.5.发送POST请求

We’ll invoke the /greetWithPost endpoint from our test as:

我们将从我们的测试中调用/greetWithPost端点作为。

http://localhost:8080/spring-mvc-test/greetWithPost

We should obtain the output:

我们应该获得输出。

{
    "id": 1,
    "message": "Hello World!!!"
}

And our test code is:

而我们的测试代码是。

@Test
public void givenGreetURIWithPost_whenMockMVC_thenVerifyResponse() {
    this.mockMvc.perform(post("/greetWithPost")).andDo(print())
      .andExpect(status().isOk()).andExpect(content()
      .contentType("application/json;charset=UTF-8"))
      .andExpect(jsonPath("$.message").value("Hello World!!!"));
}

MockMvcRequestBuilders.post(“/greetWithPost”) will send the POST request. We can set path variables and query parameters in a similar way as before, whereas form data can be set only via the param() method, similar to query parameters as:

MockMvcRequestBuilders.post(“/greetWithPost”)将发送POST请求。我们可以像以前一样设置路径变量和查询参数,而表单数据只能通过param()方法设置,与查询参数类似。

http://localhost:8080/spring-mvc-test/greetWithPostAndFormData

Then the data will be:

那么数据将是。

id=1;name=John%20Doe

So we should get:

所以我们应该得到。

{
    "id": 1,
    "message": "Hello World John Doe!!!"
}

Let’s see our test:

让我们看看我们的测试。

@Test
public void givenGreetURI_whenMockMVC_thenVerifyResponse() throws Exception {
    MvcResult mvcResult = this.mockMvc.perform(MockMvcRequestBuilders.get("/greet"))
      .andDo(print())
      .andExpect(MockMvcResultMatchers.status().isOk())
      .andExpect(MockMvcResultMatchers.jsonPath("$.message").value("Hello World!!!"))
      .andReturn();
 
   assertEquals("application/json;charset=UTF-8", mvcResult.getResponse().getContentType());
}

In the above code snippet, we’ve added two parameters: id as “1” and name as “John Doe.”

在上面的代码片断中,我们添加了两个参数。id为 “1”,name为 “John Doe”。

5. MockMvc Limitations

5.MockMvc的局限性

MockMvc provides an elegant and easy-to-use API to call web endpoints and to inspect and assert their response at the same time. Despite all its benefits, it has a few limitations.

MockMvc提供了一个优雅且易于使用的API来调用Web端点,并同时检查和断言其响应。尽管有这么多好处,但它也有一些局限性。

First of all, it does use a subclass of the DispatcherServlet to handle test requests. To be more specific, the TestDispatcherServlet is responsible for calling controllers and performing all the familiar Spring magic.

首先,它确实使用了DispatcherServlet的一个子类来处理测试请求。更具体地说,TestDispatcherServlet负责调用控制器并执行所有熟悉的Spring魔法。

The MockMvc class wraps this TestDispatcherServlet internally. So every time we send a request using the perform() method, MockMvc will use the underlying TestDispatcherServlet directly. Therefore, there are no real network connections made, and consequently, we won’t test the whole network stack while using MockMvc.

MockMvc包裹了这个TestDispatcherServlet内部。因此,每次我们使用perform()方法发送请求时,MockMvc将直接使用底层的TestDispatcherServlet。因此,没有真正的网络连接,因此,我们不会在使用MockMvc时测试整个网络栈。

Also, because Spring prepares a fake web application context to mock the HTTP requests and responses, it may not support all the features of a full-blown Spring application.

此外,由于Spring准备了一个假的Web应用程序上下文来模拟HTTP请求和响应,它可能不支持一个完整的Spring应用程序的所有功能

For example, this mock setup doesn’t support HTTP redirections. This may not seem that significant at first. However, Spring Boot handles some errors by redirecting the current request to the /error endpoint. So if we’re using the MockMvc, we may not be able to test some API failures.

例如,这个模拟设置不支持HTTP重定向。这起初可能看起来并不重要。然而,Spring Boot通过将当前请求重定向到/error 端点来处理一些错误。因此,如果我们使用MockMvc,我们可能无法测试一些API故障。

As an alternative to MockMvc, we can set up a more real application context and then use RestTemplate, or even REST-assured, to test our application.

作为MockMvc的替代方案,我们可以设置一个更真实的应用程序上下文,然后使用RestTemplate甚至REST-assured,来测试我们的应用程序。

For instance, this is easy using Spring Boot:

例如,使用Spring Boot,这很容易。

@SpringBootTest(webEnvironment = DEFINED_PORT)
public class GreetControllerRealIntegrationTest {

    @Before
    public void setUp() {
        RestAssured.port = DEFAULT_PORT;
    }

    @Test
    public void givenGreetURI_whenSendingReq_thenVerifyResponse() {
        given().get("/greet")
          .then()
          .statusCode(200);
    }
}

Here, we don’t even need to add the @ExtendWith(SpringExtension.class).

这里,我们甚至不需要添加@ExtendWith(SpringExtension.class)

This way, every test will make a real HTTP request to the application that listens on a random TCP port.

这样,每个测试都会向随机TCP端口上监听的应用程序发出一个真实的HTTP请求。

6. Conclusion

6.结论

In this article, we implemented a few simple Spring-enabled integration tests.

在这篇文章中,我们实现了几个简单的支持Spring的集成测试。

We also looked at the WebApplicationContext and MockMvc object creation, which plays an important role in calling the endpoints of the application.

我们还研究了WebApplicationContextMockMvc对象的创建,它在调用应用程序的端点方面起着重要作用。

Looking further, we discussed how to send GET and POST requests with variations of parameter passing, and how to verify the HTTP response status, header, and content.

进一步看,我们讨论了如何用参数传递的变化来发送GET和POST请求,以及如何验证HTTP响应状态、标头和内容。

Then we evaluated some limitations of MockMvc. Knowing these limitations can guide us to make an informed decision about how we’re going to implement our tests.

然后我们评估了MockMvc.的一些限制。了解这些限制可以指导我们对如何实现我们的测试做出明智的决定。

Finally, the implementation of all these examples and code snippets is available over on GitHub.

最后,所有这些例子和代码片段的实现都可以在GitHub上找到