如何 Mock(模拟)HttpServletRequest

1、概览

本文将带你了解几种模拟 HttpServletRequest 对象的方法。

首先,从 Spring Test 中的 MockHttpServletRequest 开始,这是一个功能齐全的模拟类型。然后,了解如何使用 Mockito 和 JMockit 这两个流行的模拟库进行测试。最后,了解如何使用匿名子类进行测试。

2、测试 HttpServletRequest

当我们要模拟客户端 request 信息(如 HttpServletRequest)来测试 Servlet 可能会有点麻烦,该接口定义了各种方法,需要进行实现。

要测试的目标 UserServlet 类,如下:

public class UserServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String firstName = request.getParameter("firstName");
        String lastName = request.getParameter("lastName");

        response.getWriter().append("Full Name: " + firstName + " " + lastName);
    }
}

要对 doGet() 方法进行单元测试,需要模拟 requestresponse 参数,以模拟实际的运行时行为。

3、使用 Spring 的 MockHttpServletRequest

Spring-Test 提供了一个功能齐全的类 MockHttpServletRequest,它实现了 HttpServletRequest 接口。

虽然该库主要用于测试 Spring 应用,但我们可以使用它的 MockHttpServletRequest 类,而无需实现任何 Spring 特有的功能。换句话说,即使应用不使用 Spring,我们也可以使用该依赖来模拟 HttpServletRequest 对象。

pom.xml 中这个添加 依赖

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.3.20</version>
    <scope>test</scope>
</dependency>

现在,让我们看看如何使用该类来测试 UserServlet

@Test
void givenHttpServletRequest_whenUsingMockHttpServletRequest_thenReturnsParameterValues() throws IOException {
    MockHttpServletRequest request = new MockHttpServletRequest();
    request.setParameter("firstName", "Spring");
    request.setParameter("lastName", "Test");
    MockHttpServletResponse response = new MockHttpServletResponse();

    servlet.doGet(request, response);

    assertThat(response.getContentAsString()).isEqualTo("Full Name: Spring Test");
}

如上,你可以注意到没有涉及实际的模拟。我们使用了功能完备的 requestresponse 对象,只用了几行代码就测试了目标类。因此,测试代码简洁、可读性强且易于维护。

4、使用模拟(Mock)框架

模拟框架提供了一个干净简单的API,用于测试模拟对象,这些对象模拟了原始对象的运行时行为。

它们的一些优点是表达能力强,可以直接模拟 staticprivate 方法。此外,与自定义实现相比,可以避免大部分用于模拟的样板代码,而将重点放在测试上。

4.1、Mockito

Mockito 是一个流行的开源测试自动化框架,内部使用 Java Reflection(反射) API 创建 mock 对象。

pom.xml 中添加 依赖

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>4.4.0</version>
    <scope>test</scope>
</dependency>

接下来,让我们看看如何模拟 HttpServletRequest 对象的 getParameter() 方法:

@Test
void givenHttpServletRequest_whenMockedWithMockito_thenReturnsParameterValues() throws IOException {
    // mock HttpServletRequest & HttpServletResponse
    HttpServletRequest request = mock(HttpServletRequest.class);
    HttpServletResponse response = mock(HttpServletResponse.class);

    // mock the returned value of request.getParameterMap()
    when(request.getParameter("firstName")).thenReturn("Mockito");
    when(request.getParameter("lastName")).thenReturn("Test");
    when(response.getWriter()).thenReturn(new PrintWriter(writer));

    servlet.doGet(request, response);

    assertThat(writer.toString()).isEqualTo("Full Name: Mockito Test");
}

4.2、JMockit

JMockit 是一个 Mock API,它提供了有用的记录和验证语法(可以将其用于 JUnitTestNG)。它是一个容器外集成测试库,适用于 Java EE 和基于 Spring 的应用。

让我们看看如何使用 JMockit 来模拟 HttpServletRequest

添加 jmockit 依赖:

<dependency> 
    <groupId>org.jmockit</groupId> 
    <artifactId>jmockit</artifactId> 
    <version>1.49</version>
    <scope>test</scope>
</dependency>

接下来,继续在测试类中实现模拟:

@Mocked
HttpServletRequest mockRequest;
@Mocked
HttpServletResponse mockResponse;

@Test
void givenHttpServletRequest_whenMockedWithJMockit_thenReturnsParameterValues() throws IOException {
    new Expectations() {{
        mockRequest.getParameter("firstName"); result = "JMockit";
        mockRequest.getParameter("lastName"); result = "Test";
        mockResponse.getWriter(); result = new PrintWriter(writer);
    }};

    servlet.doGet(mockRequest, mockResponse);

    assertThat(writer.toString()).isEqualTo("Full Name: JMockit Test");
}

如上所示,只需几行设置,就成功地用模拟的 HttpServletRequest 对象测试了目标类。

Mock 框架可以节省大量的工作量,并且能够更快地编写单元测试。但是,要使用模拟对象,需要了解 Mock API,并且通常需要一个单独的框架。

5、使用匿名子类

在某些项目中,可能依赖存在限制或希望直接控制自己的测试类实现。特别是在 Servlet 代码库较大的情况下,自定义实现的可重用性非常重要。在这些情况下,匿名类非常方便。

匿名类是没有名称的内部类。它们很容易实现,并且可以直接控制实际对象。如果我们不想为测试添加额外的依赖,可以考虑使用这种方法。

现在,创建一个实现 HttpServletRequest 接口的匿名子类,并用它来测试 doGet() 方法:

public static HttpServletRequest getRequest(Map<String, String[]> params) {
    return new HttpServletRequest() {
        public Map<String, String[]> getParameterMap() {
            return params;
        }

        public String getParameter(String name) {
            String[] values = params.get(name);
            if (values == null || values.length == 0) {
                return null;
            }
            return values[0];
        }

        // 其他的实现方法
    }
};

接下来,把这个 request 传递给被测试的类:

@Test
void givenHttpServletRequest_whenUsingAnonymousClass_thenReturnsParameterValues() throws IOException {
    final Map<String, String[]> params = new HashMap<>();
    params.put("firstName", new String[] { "Anonymous Class" });
    params.put("lastName", new String[] { "Test" });

    servlet.doGet(getRequest(params), getResponse(writer));

    assertThat(writer.toString()).isEqualTo("Full Name: Anonymous Class Test");
}

这种解决方案的缺点是需要创建一个匿名类,并对所有抽象方法进行虚拟实现。此外,嵌套对象(如 HttpSession)有可能需要特定的实现。

6、总结

本文介绍了在编写 servlet 单元测试时模拟 HttpServletRequest 对象的几种方式。除了使用模拟框架外,还可以使用 MockHttpServletRequest 类进行测试,这更简洁高效。


参考:https://www.baeldung.com/java-httpservletrequest-mock