单元测试Java Servlet

单元测试Java Servlet

Unit testing a Java Servlet

我想知道什么是对Servlet进行单元测试的最佳方法。

只要测试内部方法不引用servlet上下文,就不会有问题,但是测试doGet / doPost方法以及引用该上下文或使用会话参数的内部方法又如何呢?

有没有一种方法可以简单地使用经典工具(例如JUnit或最好是TestNG)来做到这一点? 我是否需要嵌入tomcat服务器或类似的东西?


大多数时候,我都是通过"集成测试"而不是纯粹的单元测试来测试Servlet和JSP。有大量的JUnit / TestNG插件,包括:

  • HttpUnit(最古老和最著名的,非常低的级别,根据您的需要可能是好是坏)
  • HtmlUnit(比HttpUnit更高的级别,对于许多项目来说更好)
  • JWebUnit(位于其他测试工具之上,并试图简化它们-我更喜欢这种工具)
  • WatiJ和Selenium(使用您的浏览器进行测试,虽然更重但也很实际)

这是针对简单的Order Processing Servlet的JWebUnit测试,该Servlet处理来自" orderEntry.html"形式的输入。它需要一个客户ID,一个客户名称和一个或多个订单商品:

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
public class OrdersPageTest {
    private static final String WEBSITE_URL ="http://localhost:8080/demo1";

    @Before
    public void start() {
        webTester = new WebTester();
        webTester.setTestingEngineKey(TestingEngineRegistry.TESTING_ENGINE_HTMLUNIT);
        webTester.getTestContext().setBaseUrl(WEBSITE_URL);
    }
    @Test
    public void sanity() throws Exception {
        webTester.beginAt("/orderEntry.html");
        webTester.assertTitleEquals("Order Entry Form");
    }
    @Test
    public void idIsRequired() throws Exception {
        webTester.beginAt("/orderEntry.html");
        webTester.submit();
        webTester.assertTextPresent("ID Missing!");
    }
    @Test
    public void nameIsRequired() throws Exception {
        webTester.beginAt("/orderEntry.html");
        webTester.setTextField("id","AB12");
        webTester.submit();
        webTester.assertTextPresent("Name Missing!");
    }
    @Test
    public void validOrderSucceeds() throws Exception {
        webTester.beginAt("/orderEntry.html");
        webTester.setTextField("id","AB12");
        webTester.setTextField("name","Joe Bloggs");

        //fill in order line one
        webTester.setTextField("lineOneItemNumber","AA");
        webTester.setTextField("lineOneQuantity","12");
        webTester.setTextField("lineOneUnitPrice","3.4");

        //fill in order line two
        webTester.setTextField("lineTwoItemNumber","BB");
        webTester.setTextField("lineTwoQuantity","14");
        webTester.setTextField("lineTwoUnitPrice","5.6");

        webTester.submit();
        webTester.assertTextPresent("Total: 119.20");
    }
    private WebTester webTester;
}

尝试使用HttpUnit,尽管您最终可能会写自动化测试,该测试比模块的"单元测试"(单个类的)更"集成测试"(模块的)。


我查看了发布的答案,以为我会发布一个更完整的解决方案,该解决方案实际上演示了如何使用嵌入式GlassFish及其Apache Maven插件进行测试。

我在自己的博客上使用"使用带有JUnit 4.x和HtmlUnit 2.x的Embedded GlassFish 3.1.1嵌入式"编写了完整的过程,并将完整的项目放在此处的Bitbucket上进行下载:image-servlet

在看到这个问题之前,我正在看关于JSP / JSF标签的图像servlet的另一篇文章。因此,我将我在其他文章中使用的解决方案与该文章的完整的单元测试版本结合在一起。

测试方法

Apache Maven具有明确定义的生命周期,其中包括test。我将使用它和另一个称为integration-test的生命周期来实现我的解决方案。

  • 在surefire插件中禁用标准生命周期单元测试。
  • integration-test添加为surefire插件执行的一部分
  • 将GlassFish Maven插件添加到POM。
  • 配置GlassFish在integration-test生命周期内执行。
  • 运行单元测试(集成测试)。
  • GlassFish插件

    将此插件添加为的一部分。

    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
            <plugin>
                <groupId>org.glassfish</groupId>
                maven-embedded-glassfish-plugin</artifactId>
                <version>3.1.1</version>
                <configuration>
                    <!-- This sets the path to use the war file we have built in the target directory -->
                    target/${project.build.finalName}</app>
                    <port>8080</port>
                    <!-- This sets the context root, e.g. http://localhost:8080/test/ -->
                    <contextRoot>test</contextRoot>
                    <!-- This deletes the temporary files during GlassFish shutdown. -->
                    true</autoDelete>
                </configuration>
                <executions>
                    <execution>
                        <id>start</id>
                        <!-- We implement the integration testing by setting up our GlassFish instance to start and deploy our application. -->
                        <phase>pre-integration-test</phase>
                        <goals>
                            <goal>start</goal>
                            <goal>deploy</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>stop</id>
                        <!-- After integration testing we undeploy the application and shutdown GlassFish gracefully. -->
                        <phase>post-integration-test</phase>
                        <goals>
                            <goal>undeploy</goal>
                            <goal>stop</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

    Surefire插件

    作为的一部分添加/修改插件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                maven-surefire-plugin</artifactId>
                <version>2.12.4</version>
                <!-- We are skipping the default test lifecycle and will test later during integration-test -->
                <configuration>
                    <skip>true</skip>
                </configuration>
                <executions>
                    <execution>
                        <phase>integration-test</phase>
                        <goals>
                            <!-- During the integration test we will execute surefire:test -->
                            <goal>test</goal>
                        </goals>
                        <configuration>
                            <!-- This enables the tests which were disabled previously. -->
                            <skip>false</skip>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

    HTMLUnit

    添加集成测试,如下例所示。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Test
    public void badRequest() throws IOException {
        webClient.getOptions().setThrowExceptionOnFailingStatusCode(false);
        webClient.getOptions().setPrintContentOnFailingStatusCode(false);
        final HtmlPage page = webClient.getPage("http://localhost:8080/test/images/");
        final WebResponse response = page.getWebResponse();
        assertEquals(400, response.getStatusCode());
        assertEquals("An image name is required.", response.getStatusMessage());
        webClient.getOptions().setThrowExceptionOnFailingStatusCode(true);
        webClient.getOptions().setPrintContentOnFailingStatusCode(true);
        webClient.closeAllWindows();
    }

    我在自己的博客上使用"使用带有JUnit 4.x和HtmlUnit 2.x的Embedded GlassFish 3.1.1嵌入式"编写了完整的过程,并将完整的项目放在此处的Bitbucket上进行下载:image-servlet

    如有任何疑问,请发表评论。我认为这是一个完整的示例,可供您用作计划针对servlet进行的任何测试的基础。


    Mockrunner(http://mockrunner.sourceforge.net/index.html)可以做到这一点。它提供了一个可用于测试Servlet的模拟J2EE容器。它还可以用于对其他服务器端代码(如EJB,JDBC,JMS,Struts)进行单元测试。我本人仅使用了JDBC和EJB功能。


    您是否在单元测试中手动调用doPost和doGet方法?如果是这样,您可以重写HttpServletRequest方法以提供模拟对象。

    1
    2
    3
    4
    5
    6
    7
    myServlet.doGet(new HttpServletRequestWrapper() {
         public HttpSession getSession() {
             return mockSession;
         }

         ...
    }

    HttpServletRequestWrapper是一个便捷的Java类。我建议您在单元测试中创建一个实用程序方法来创建模拟http请求:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public void testSomething() {
        myServlet.doGet(createMockRequest(), createMockResponse());
    }

    protected HttpServletRequest createMockRequest() {
       HttpServletRequest request = new HttpServletRequestWrapper() {
            //overrided methods  
       }
    }

    最好将模拟创建方法放在基础servlet超类中,并进行所有servlet单元测试以对其进行扩展。


    Servlet doPost()方法的JUnit测试的此实现仅依赖于Mockito库来模拟HttpRequestHttpResponseHttpSessionServletResponseRequestDispatcher的实例。将参数键和JavaBean实例替换为与从其调用doPost()的关联JSP文件中引用的值相对应的那些参数。

    Mockito Maven依赖项:

    1
    2
    3
    4
    5
    <dependency>
          <groupId>org.mockito</groupId>
          mockito-all</artifactId>
          <version>1.9.5</version>
    </dependency>

    JUnit测试:

    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
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    import javax.servlet.RequestDispatcher;
    import javax.servlet.ServletContext;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import javax.servlet.http.HttpSession;

    import java.io.IOException;

    import static org.junit.Assert.assertFalse;
    import static org.junit.Assert.assertTrue;
    import static org.mockito.Mockito.*;

    /**
     * Unit tests for the {@code StockSearchServlet} class.
     * @author Bob Basmaji
     */

    public class StockSearchServletTest extends HttpServlet {
        // private fields of this class
        private static HttpServletRequest request;
        private static HttpServletResponse response;
        private static StockSearchServlet servlet;
        private static final String SYMBOL_PARAMETER_KEY ="symbol";
        private static final String STARTRANGE_PARAMETER_KEY ="startRange";
        private static final String ENDRANGE_PARAMETER_KEY ="endRange";
        private static final String INTERVAL_PARAMETER_KEY ="interval";
        private static final String SERVICETYPE_PARAMETER_KEY ="serviceType";

        /**
         * Sets up the logic common to each test in this class
         */

        @Before
        public final void setUp() {
            request = mock(HttpServletRequest.class);
            response = mock(HttpServletResponse.class);

            when(request.getParameter("symbol"))
                    .thenReturn("AAPL");

            when(request.getParameter("startRange"))
                    .thenReturn("2016-04-23 00:00:00");

            when(request.getParameter("endRange"))
                    .thenReturn("2016-07-23 00:00:00");

            when(request.getParameter("interval"))
                    .thenReturn("DAY");

            when(request.getParameter("serviceType"))
                    .thenReturn("WEB");

            String symbol = request.getParameter(SYMBOL_PARAMETER_KEY);
            String startRange = request.getParameter(STARTRANGE_PARAMETER_KEY);
            String endRange = request.getParameter(ENDRANGE_PARAMETER_KEY);
            String interval = request.getParameter(INTERVAL_PARAMETER_KEY);
            String serviceType = request.getParameter(SERVICETYPE_PARAMETER_KEY);

            HttpSession session = mock(HttpSession.class);
            when(request.getSession()).thenReturn(session);
            final ServletContext servletContext = mock(ServletContext.class);
            RequestDispatcher dispatcher = mock(RequestDispatcher.class);
            when(servletContext.getRequestDispatcher("/stocksearchResults.jsp")).thenReturn(dispatcher);
            servlet = new StockSearchServlet() {
                public ServletContext getServletContext() {
                    return servletContext; // return the mock
                }
            };

            StockSearchBean search = new StockSearchBean(symbol, startRange, endRange, interval);
            try {
                switch (serviceType) {
                    case ("BASIC"):
                        search.processData(ServiceType.BASIC);
                        break;
                    case ("DATABASE"):
                        search.processData(ServiceType.DATABASE);
                        break;
                    case ("WEB"):
                        search.processData(ServiceType.WEB);
                        break;
                    default:
                        search.processData(ServiceType.WEB);
                }
            } catch (StockServiceException e) {
                throw new RuntimeException(e.getMessage());
            }
            session.setAttribute("search", search);
        }

        /**
         * Verifies that the doPost method throws an exception when passed null arguments
         * @throws ServletException
         * @throws IOException
         */

        @Test(expected = NullPointerException.class)
        public final void testDoPostPositive() throws ServletException, IOException {
            servlet.doPost(null, null);
        }

        /**
         * Verifies that the doPost method runs without exception
         * @throws ServletException
         * @throws IOException
         */

        @Test
        public final void testDoPostNegative() throws ServletException, IOException {
            boolean throwsException = false;
            try {
                servlet.doPost(request, response);
            } catch (Exception e) {
                throwsException = true;
            }
            assertFalse("doPost throws an exception", throwsException);
        }
    }

    2018年2月更新:OpenBrace Limited已关闭,并且不再支持其ObMimic产品。

    另一个解决方案是使用我的ObMimic库,该库专门用于Servlet的单元测试。它提供了所有Servlet API类的完整的纯Java实现,并且您可以根据需要配置和检查它们,以进行测试。

    您确实可以使用它直接从JUnit或TestNG测试中调用doGet / doPost方法,并测试任何内部方法,即使它们引用ServletContext或使用会话参数(或任何其他Servlet API功能)也是如此。

    这不需要外部或嵌入式容器,也不会限制您进行基于HTTP的更广泛的"集成"测试,并且与通用模拟不同,它具有"嵌入"的完整Servlet API行为,因此您的测试可以是"基于状态"而不是基于"交互"(例如,您的测试不必依赖于代码进行的Servlet API调用的确切顺序,也不必依赖于自己对Servlet API如何响应每个调用的期望) 。

    我对如何使用JUnit测试servlet的回答中有一个简单的示例。有关完整细节和免费下载,请访问ObMimic网站。


    推荐阅读