【问题标题】:Unit testing a Java Servlet对 Java Servlet 进行单元测试
【发布时间】:2010-09-10 14:13:54
【问题描述】:

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

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

有没有一种方法可以简单地使用 JUnit 等经典工具,或者最好使用 TestNG?我需要嵌入 tomcat 服务器或类似的东西吗?

【问题讨论】:

标签: java unit-testing servlets


【解决方案1】:

大多数时候,我通过“集成测试”而不是纯单元测试来测试 Servlet 和 JSP。有大量适用于 JUnit/TestNG 的附加组件,包括:

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

这是一个针对简单订单处理 Servlet 的 JWebUnit 测试,该 Servlet 处理来自“orderEntry.html”表单的输入。它需要一个客户 ID、一个客户名称和一个或多个订单项:

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;
}

【讨论】:

    【解决方案2】:

    尝试HttpUnit,尽管您最终编写的自动化测试可能更像是(模块的)“集成测试”而不是(单个类的)“单元测试”。

    【讨论】:

    • 嗯,这实际上更多是关于单元测试,因为如果可能的话,我会通过与 mock 的交互来替换 servlet 类与其他类之间的所有交互。
    • HttpUnit 自 2008 年以来似乎没有任何变化,这表明它是一个死项目。
    • HttpUnit 有更新的替代品吗?
    • @Raedwald HttpUnit 没有死,见:stackoverflow.com/questions/8630395/…
    • 发现 HttpUnit(至少 Maven 上的包)不适用于使用 getServletContext() 的 servlet。抛出 java.lang.NoSuchMethodError。
    【解决方案3】:

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

    我在我的博客Using GlassFish 3.1.1 Embedded with JUnit 4.x and HtmlUnit 2.x 上写了完整的过程,并将完整的项目放在Bitbucket 上供下载:image-servlet

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

    如何测试

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

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

    GlassFish 插件

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

            <plugin>
                <groupId>org.glassfish</groupId>
                <artifactId>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 -->
                    <app>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. -->
                    <autoDelete>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 插件

    添加/修改插件作为&lt;build&gt; 的一部分。

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>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>
    

    HTML 单元

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

    @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();
    }
    

    我将完整的过程写在我的博客Using GlassFish 3.1.1 Embedded with JUnit 4.x and HtmlUnit 2.x 上,并将完整的项目放在Bitbucket 上供下载:image-servlet

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

    【讨论】:

      【解决方案4】:

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

      myServlet.doGet(new HttpServletRequestWrapper() {
           public HttpSession getSession() {
               return mockSession;
           }
      
           ...
      }
      

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

      public void testSomething() {
          myServlet.doGet(createMockRequest(), createMockResponse());
      }
      
      protected HttpServletRequest createMockRequest() {
         HttpServletRequest request = new HttpServletRequestWrapper() {
              //overrided methods   
         }
      }
      

      最好将模拟创建方法放在基本 servlet 超类中,并让所有 servlet 单元测试来扩展它。

      【讨论】:

      • HttpServletRequestWrapper 没有默认构造函数,只有一个带有 HttpServletRequest 参数的构造函数。
      【解决方案5】:

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

      【讨论】:

      • mockrunner 自 2009 年以来一直没有更新。是否正在维护替代方案?
      【解决方案6】:

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

      Mockito Maven 依赖项:

      <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-all</artifactId>
            <version>1.9.5</version>
      </dependency>
      

      JUnit 测试:

      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);
          }
      }
      

      【讨论】:

      • verify(session).setAttribute("field", "value") 也可能是一个很好的断言。
      【解决方案7】:

      2018 年 2 月更新:OpenBrace Limited has closed down,不再支持其 ObMimic 产品。

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

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

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

      在我对How to test my servlet using JUnit 的回答中有一个简单的例子。如需完整详情和免费下载,请访问ObMimic 网站。

      【讨论】:

      • "要安装 ObMimic:将 ObMimic-1.1.000.zip 压缩包解压缩到..."等等,什么?!
      【解决方案8】:

      这个问题有一个解决方案提出 Mockito How to test my servlet using JUnit 这将任务限制为简单的单元测试,无需设置任何类似服务器的环境。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-12-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-11-22
        • 1970-01-01
        相关资源
        最近更新 更多