Java Servlet的单元测试

54
我想知道如何最好地对Servlet执行单元测试。测试内部方法并不成问题,只要它们不引用Servlet上下文,但是如果要测试doGet/doPost方法以及引用上下文或使用会话参数的内部方法,怎么办呢?是否可以使用传统的工具,例如JUnit或者更好的TestNG来简单地完成这个任务?我需要嵌入Tomcat服务器或类似的东西吗?

1
可能是单元测试Servlet的重复问题。 - Raedwald
8个回答

46

通常我使用“集成测试”来测试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;
}

13

尝试使用HttpUnit,虽然您可能最终会编写更多针对模块的“集成测试”自动化测试,而不是单个类的“单元测试”。


嗯,这更多是关于单元测试的问题,如果可能的话,我会用模拟对象来替代与servlet类之间的所有交互。 - gizmo
2
HttpUnit自2008年以来似乎没有任何变化,这表明它是一个已经停止开发的项目。 - Raedwald
3
有没有HttpUnit的新替代品? - oconnor0
1
@Raedwald HttpUnit并没有死,可以看看这里:https://dev59.com/Wl7Va4cB1Zd3GeqPLqIh - Guy
发现HttpUnit(至少是Maven上的包)无法与使用getServletContext()的servlet一起使用。会抛出java.lang.NoSuchMethodError异常。 - Mike

11

我看了已发布的答案,想发表一个更完整的解决方案,该方案实际演示了如何使用嵌入式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-plugin执行的一部分。
  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>

可靠的插件

将插件添加/修改为<build>的一部分。

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

HTMLUnit

像下面的示例一样添加集成测试。

@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();
}
我在我的博客上写了完整的过程,您可以访问使用 GlassFish 3.1.1 嵌入 JUnit 4.x 和 HtmlUnit 2.x 进行测试。并将完整项目放在 Bitbucket 的image-servlet处供下载。
如果您有任何问题,请留言。我认为这是一个完整的示例,可以作为您计划进行 servlet 测试的基础。

7
你在单元测试中手动调用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单元测试都继承它。


4
HttpServletRequestWrapper没有默认构造函数,只有一个带有HttpServletRequest参数的构造函数。 - antony.trupe

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

2
Mockrunner自2009年以来就没有更新了。有没有正在维护的替代品? - datguy

3

这是一个关于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") 也可能是一个很好的断言。 - Mahdi

0

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网站。


安装ObMimic:将ObMimic-1.1.000.zip归档文件解压缩到您想要安装的位置...等等,什么? - Innokenty

0

这个问题有一个解决方案,提出了使用Mockito 如何使用JUnit测试我的Servlet的建议。这将限制任务只进行简单的单元测试,而不需要设置任何类似服务器的环境。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接