如何使用 JUnit 测试我的 servlet

Posted

技术标签:

【中文标题】如何使用 JUnit 测试我的 servlet【英文标题】:How to test my servlet using JUnit 【发布时间】:2011-07-23 00:12:25 【问题描述】:

我已经使用 Java Servlet 创建了一个 Web 系统,现在想进行 JUnit 测试。我的dataManager 只是将其提交到数据库的一段基本代码。您将如何使用 JUnit 测试 Servlet?

我的代码示例允许用户注册/注册,它是通过 AJAX 从我的主页提交的:

public void doPost(HttpServletRequest request, HttpServletResponse response) 
         throws ServletException, IOException

    // Get parameters
    String userName = request.getParameter("username");
    String password = request.getParameter("password");
    String name = request.getParameter("name");

    try 

        // Load the database driver
        Class.forName("com.mysql.jdbc.Driver");

        //pass reg details to datamanager       
        dataManager = new DataManager();
        //store result as string
        String result = dataManager.register(userName, password, name);

        //set response to html + no cache
        response.setContentType("text/html");
        response.setHeader("Cache-Control", "no-cache");
        //send response with register result
        response.getWriter().write(result);

     catch(Exception e)
        System.out.println("Exception is :" + e);
      

【问题讨论】:

【参考方案1】:

您可以使用Mockito 执行此操作,让模拟返回正确的参数,验证它们确实被调用(可选地指定次数),写入“结果”并验证它是否正确。

import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import java.io.*;
import javax.servlet.http.*;
import org.apache.commons.io.FileUtils;
import org.junit.Test;

public class TestMyServlet extends Mockito

    @Test
    public void testServlet() throws Exception 
        HttpServletRequest request = mock(HttpServletRequest.class);       
        HttpServletResponse response = mock(HttpServletResponse.class);    

        when(request.getParameter("username")).thenReturn("me");
        when(request.getParameter("password")).thenReturn("secret");

        StringWriter stringWriter = new StringWriter();
        PrintWriter writer = new PrintWriter(stringWriter);
        when(response.getWriter()).thenReturn(writer);

        new MyServlet().doPost(request, response);

        verify(request, atLeast(1)).getParameter("username"); // only if you want to verify username was called...
        writer.flush(); // it may not have been flushed yet...
        assertTrue(stringWriter.toString().contains("My expected string"));
    

【讨论】:

这样,如何确保响应时设置“Cache-Control”? 您可以使用 StringWriter(作为 PrintWriter 构造函数的参数),而不是打印到磁盘上的实际文件。然后你会 assertTrue(stringWriter.toString().contains("My Expected String"));这样,测试将读取/写入内存而不是磁盘。 @aaronvargas:谢谢你的回答!但是当我执行您的代码时,我收到以下错误: java.util.MissingResourceException: Can't find bundle for base name javax.servlet.LocalStrings, locale de_DE - 它发生在执行 new MyServlet().doPost( ...)。知道什么会被破坏吗? @BennyNeugebauer,听起来包不在类路径上。我会编写另一个 JUnit 测试,它只从 Bundle 中获取一个值来隔离问题。 @aaronvargas,感谢您的反馈!我找到了解决方案。我不得不“javax.servlet-api”到我的 pom.xml 中的依赖项。【参考方案2】:

首先,在真正的应用程序中,您永远不会在 servlet 中获得数据库连接信息;你可以在你的应用服务器中配置它。

但是,有一些方法可以在不运行容器的情况下测试 Servlet。一种是使用模拟对象。 Spring 为 HttpServletRequest、HttpServletResponse、HttpServletSession 等提供了一组非常有用的模拟:

http://static.springsource.org/spring/docs/3.0.x/api/org/springframework/mock/web/package-summary.html

使用这些模拟,您可以测试诸如

如果用户名不在请求中会怎样?

如果用户名在请求中会发生什么?

然后您可以执行以下操作:

import static org.junit.Assert.assertEquals;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.junit.Before;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;

public class MyServletTest 
    private MyServlet servlet;
    private MockHttpServletRequest request;
    private MockHttpServletResponse response;

    @Before
    public void setUp() 
        servlet = new MyServlet();
        request = new MockHttpServletRequest();
        response = new MockHttpServletResponse();
    

    @Test
    public void correctUsernameInRequest() throws ServletException, IOException 
        request.addParameter("username", "scott");
        request.addParameter("password", "tiger");

        servlet.doPost(request, response);

        assertEquals("text/html", response.getContentType());

        // ... etc
    

【讨论】:

这给了我 java.lang.NoClassDefFoundError: javax/servlet/http/HttpServletMapping【参考方案3】:

我发现 Selenium 测试对于集成或功能(端到端)测试更有用。我正在尝试使用 org.springframework.mock.web,但我并没有走得太远。我正在附加一个带有 jMock 测试套件的示例控制器。

首先,控制器:

package com.company.admin.web;

import javax.validation.Valid;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionStatus;

import com.company.admin.domain.PaymentDetail;
import com.company.admin.service.PaymentSearchService;
import com.company.admin.service.UserRequestAuditTrail;
import com.company.admin.web.form.SearchCriteria;

/**
 * Controls the interactions regarding to the refunds.
 * 
 * @author slgelma
 *
 */
@Controller
@SessionAttributes("user", "authorization")
public class SearchTransactionController 

    public static final String SEARCH_TRANSACTION_PAGE = "searchtransaction";

    private PaymentSearchService searchService;
    //private Validator searchCriteriaValidator;
    private UserRequestAuditTrail notifications;

    @Autowired
    public void setSearchService(PaymentSearchService searchService) 
        this.searchService = searchService;
    

    @Autowired
    public void setNotifications(UserRequestAuditTrail notifications) 
        this.notifications = notifications;
    

    @RequestMapping(value="/" + SEARCH_TRANSACTION_PAGE)
    public String setUpTransactionSearch(Model model) 
        SearchCriteria searchCriteria = new SearchCriteria();
        model.addAttribute("searchCriteria", searchCriteria);
        notifications.transferTo(SEARCH_TRANSACTION_PAGE);
        return SEARCH_TRANSACTION_PAGE;
    

    @RequestMapping(value="/" + SEARCH_TRANSACTION_PAGE, method=RequestMethod.POST, params="cancel")
    public String cancelSearch() 
        notifications.redirectTo(HomeController.HOME_PAGE);
        return "redirect:/" + HomeController.HOME_PAGE;
    

    @RequestMapping(value="/" + SEARCH_TRANSACTION_PAGE, method=RequestMethod.POST, params="execute")
    public String executeSearch(
            @ModelAttribute("searchCriteria") @Valid SearchCriteria searchCriteria,
            BindingResult result, Model model,
            SessionStatus status) 
        //searchCriteriaValidator.validate(criteria, result);
        if (result.hasErrors()) 
            notifications.transferTo(SEARCH_TRANSACTION_PAGE);
            return SEARCH_TRANSACTION_PAGE;
         else 
            PaymentDetail payment = 
                searchService.getAuthorizationFor(searchCriteria.geteWiseTransactionId());
            if (payment == null) 
                ObjectError error = new ObjectError(
                        "eWiseTransactionId", "Transaction not found");
                result.addError(error);
                model.addAttribute("searchCriteria", searchCriteria);
                notifications.transferTo(SEARCH_TRANSACTION_PAGE);
                return SEARCH_TRANSACTION_PAGE;
             else 
                model.addAttribute("authorization", payment);
                notifications.redirectTo(PaymentDetailController.PAYMENT_DETAIL_PAGE);
                return "redirect:/" + PaymentDetailController.PAYMENT_DETAIL_PAGE;
            
        
    


接下来,测试:

    package test.unit.com.company.admin.web;

    import static org.hamcrest.Matchers.containsString;
    import static org.hamcrest.Matchers.equalTo;
    import static org.junit.Assert.assertThat;

    import org.jmock.Expectations;
    import org.jmock.Mockery;
    import org.jmock.integration.junit4.JMock;
    import org.jmock.integration.junit4.JUnit4Mockery;
    import org.junit.Before;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.ui.Model;
    import org.springframework.validation.BindingResult;
    import org.springframework.validation.ObjectError;
    import org.springframework.web.bind.support.SessionStatus;

    import com.company.admin.domain.PaymentDetail;
    import com.company.admin.service.PaymentSearchService;
    import com.company.admin.service.UserRequestAuditTrail;
    import com.company.admin.web.HomeController;
    import com.company.admin.web.PaymentDetailController;
    import com.company.admin.web.SearchTransactionController;
    import com.company.admin.web.form.SearchCriteria;

    /**
     * Tests the behavior of the SearchTransactionController.
     * @author slgelma
     *
     */
    @RunWith(JMock.class)
    public class SearchTransactionControllerTest 

        private final Mockery context = new JUnit4Mockery(); 
        private final SearchTransactionController controller = new SearchTransactionController();
        private final PaymentSearchService searchService = context.mock(PaymentSearchService.class);
        private final UserRequestAuditTrail notifications = context.mock(UserRequestAuditTrail.class);
        private final Model model = context.mock(Model.class);


        /**
         * @throws java.lang.Exception
         */
        @Before
        public void setUp() throws Exception 
            controller.setSearchService(searchService);
            controller.setNotifications(notifications);
        

        @Test
        public void setUpTheSearchForm() 

            final String target = SearchTransactionController.SEARCH_TRANSACTION_PAGE;

            context.checking(new Expectations() 
                oneOf(model).addAttribute(
                        with(any(String.class)), with(any(Object.class)));
                oneOf(notifications).transferTo(with(any(String.class)));
            );

            String nextPage = controller.setUpTransactionSearch(model);
            assertThat("Controller is not requesting the correct form", 
                    target, equalTo(nextPage));
        

        @Test
        public void cancelSearchTest() 

            final String target = HomeController.HOME_PAGE;

            context.checking(new Expectations()
                never(model).addAttribute(with(any(String.class)), with(any(Object.class)));
                oneOf(notifications).redirectTo(with(any(String.class)));
            );

            String nextPage = controller.cancelSearch();
            assertThat("Controller is not requesting the correct form", 
                    nextPage, containsString(target));
        

        @Test
        public void executeSearchWithNullTransaction() 

            final String target = SearchTransactionController.SEARCH_TRANSACTION_PAGE;

            final SearchCriteria searchCriteria = new SearchCriteria();
            searchCriteria.seteWiseTransactionId(null);

            final BindingResult result = context.mock(BindingResult.class);
            final SessionStatus status = context.mock(SessionStatus.class);

            context.checking(new Expectations() 
                allowing(result).hasErrors(); will(returnValue(true));
                never(model).addAttribute(with(any(String.class)), with(any(Object.class)));
                never(searchService).getAuthorizationFor(searchCriteria.geteWiseTransactionId());
                oneOf(notifications).transferTo(with(any(String.class)));
            );

            String nextPage = controller.executeSearch(searchCriteria, result, model, status);
            assertThat("Controller is not requesting the correct form", 
                    target, equalTo(nextPage));
        

        @Test
        public void executeSearchWithEmptyTransaction() 

            final String target = SearchTransactionController.SEARCH_TRANSACTION_PAGE;

            final SearchCriteria searchCriteria = new SearchCriteria();
            searchCriteria.seteWiseTransactionId("");

            final BindingResult result = context.mock(BindingResult.class);
            final SessionStatus status = context.mock(SessionStatus.class);

            context.checking(new Expectations() 
                allowing(result).hasErrors(); will(returnValue(true));
                never(model).addAttribute(with(any(String.class)), with(any(Object.class)));
                never(searchService).getAuthorizationFor(searchCriteria.geteWiseTransactionId());
                oneOf(notifications).transferTo(with(any(String.class)));
            );

            String nextPage = controller.executeSearch(searchCriteria, result, model, status);
            assertThat("Controller is not requesting the correct form", 
                    target, equalTo(nextPage));
        

        @Test
        public void executeSearchWithTransactionNotFound() 

            final String target = SearchTransactionController.SEARCH_TRANSACTION_PAGE;
            final String badTransactionId = "badboy"; 
            final PaymentDetail transactionNotFound = null;

            final SearchCriteria searchCriteria = new SearchCriteria();
            searchCriteria.seteWiseTransactionId(badTransactionId);

            final BindingResult result = context.mock(BindingResult.class);
            final SessionStatus status = context.mock(SessionStatus.class);

            context.checking(new Expectations() 
                allowing(result).hasErrors(); will(returnValue(false));
                atLeast(1).of(model).addAttribute(with(any(String.class)), with(any(Object.class)));
                oneOf(searchService).getAuthorizationFor(with(any(String.class)));
                    will(returnValue(transactionNotFound));
                oneOf(result).addError(with(any(ObjectError.class)));
                oneOf(notifications).transferTo(with(any(String.class)));
            );

            String nextPage = controller.executeSearch(searchCriteria, result, model, status);
            assertThat("Controller is not requesting the correct form", 
                    target, equalTo(nextPage));
        

        @Test
        public void executeSearchWithTransactionFound() 

            final String target = PaymentDetailController.PAYMENT_DETAIL_PAGE;
            final String goodTransactionId = "100000010";
            final PaymentDetail transactionFound = context.mock(PaymentDetail.class);

            final SearchCriteria searchCriteria = new SearchCriteria();
            searchCriteria.seteWiseTransactionId(goodTransactionId);

            final BindingResult result = context.mock(BindingResult.class);
            final SessionStatus status = context.mock(SessionStatus.class);

            context.checking(new Expectations() 
                allowing(result).hasErrors(); will(returnValue(false));
                atLeast(1).of(model).addAttribute(with(any(String.class)), with(any(Object.class)));
                oneOf(searchService).getAuthorizationFor(with(any(String.class)));
                    will(returnValue(transactionFound));
                oneOf(notifications).redirectTo(with(any(String.class)));
            );

            String nextPage = controller.executeSearch(searchCriteria, result, model, status);
            assertThat("Controller is not requesting the correct form", 
                    nextPage, containsString(target));
        

    

我希望这可能会有所帮助。

【讨论】:

【参考方案4】:

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

这是另一种选择,使用 OpenBrace 的 ObMimic Servlet API 测试替身库(披露:我是它的开发者)。

package com.openbrace.experiments.examplecode.***5434419;

import static org.junit.Assert.*;
import com.openbrace.experiments.examplecode.***5434419.YourServlet;
import com.openbrace.obmimic.mimic.servlet.ServletConfigMimic;
import com.openbrace.obmimic.mimic.servlet.http.HttpServletRequestMimic;
import com.openbrace.obmimic.mimic.servlet.http.HttpServletResponseMimic;
import com.openbrace.obmimic.substate.servlet.RequestParameters;
import org.junit.Before;
import org.junit.Test;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * Example tests for @link YourServlet#doPost(HttpServletRequest,
 * HttpServletResponse).
 *
 * @author Mike Kaufman, OpenBrace Limited
 */
public class YourServletTest 

    /** The servlet to be tested by this instance's test. */
    private YourServlet servlet;

    /** The "mimic" request to be used in this instance's test. */
    private HttpServletRequestMimic request;

    /** The "mimic" response to be used in this instance's test. */
    private HttpServletResponseMimic response;

    /**
     * Create an initialized servlet and a request and response for this
     * instance's test.
     *
     * @throws ServletException if the servlet's init method throws such an
     *     exception.
     */
    @Before
    public void setUp() throws ServletException 
        /*
         * Note that for the simple servlet and tests involved:
         * - We don't need anything particular in the servlet's ServletConfig.
         * - The ServletContext isn't relevant, so ObMimic can be left to use
         *   its default ServletContext for everything.
         */
        servlet = new YourServlet();
        servlet.init(new ServletConfigMimic());
        request = new HttpServletRequestMimic();
        response = new HttpServletResponseMimic();
    

    /**
     * Test the doPost method with example argument values.
     *
     * @throws ServletException if the servlet throws such an exception.
     * @throws IOException if the servlet throws such an exception.
     */
    @Test
    public void testYourServletDoPostWithExampleArguments()
            throws ServletException, IOException 

        // Configure the request. In this case, all we need are the three
        // request parameters.
        RequestParameters parameters
            = request.getMimicState().getRequestParameters();
        parameters.set("username", "mike");
        parameters.set("password", "xyz#zyx");
        parameters.set("name", "Mike");

        // Run the "doPost".
        servlet.doPost(request, response);

        // Check the response's Content-Type, Cache-Control header and
        // body content.
        assertEquals("text/html; charset=ISO-8859-1",
            response.getMimicState().getContentType());
        assertArrayEquals(new String[]  "no-cache" ,
            response.getMimicState().getHeaders().getValues("Cache-Control"));
        assertEquals("...expected result from dataManager.register...",
            response.getMimicState().getBodyContentAsString());

    


注意事项:

每个“模仿”都有一个“模仿状态”对象用于其逻辑状态。这提供了 Servlet API 方法与模拟内部状态的配置和检查之间的明显区别。

您可能会惊讶于 Content-Type 的检查包括“charset=ISO-8859-1”。但是,对于给定的“doPost”代码,这是根据 Servlet API Javadoc 和 HttpServletResponse 自己的 getContentType 方法,以及在例如生成的实际 Content-Type 标头。 Glassfish 3. 如果使用普通的模拟对象和您自己对 API 行为的期望,您可能不会意识到这一点。在这种情况下,这可能无关紧要,但在更复杂的情况下,这是一种意想不到的 API 行为,可能会让人有点嘲笑!

我使用response.getMimicState().getContentType() 作为检查 Content-Type 并说明上述要点的最简单方法,但如果您愿意,您确实可以自行检查“text/html”(使用 response.getMimicState().getContentTypeMimeType() )。检查 Content-Type 标头的方法与检查 Cache-Control 标头相同。

对于此示例,响应内容被检查为字符数据(使用 Writer 的编码)。我们还可以检查是否使用了响应的 Writer 而不是它的 OutputStream(使用response.getMimicState().isWritingCharacterContent()),但我认为我们只关心结果输出,而不关心生成它的 API 调用(尽管这也可以检查...)。也可以将响应的正文内容作为字节检索,检查 Writer/OutputStream 的详细状态等。

在OpenBrace 网站上有 ObMimic 的完整详细信息和免费下载。或者如果您有任何问题可以联系我(联系方式在网站上)。

【讨论】:

【参考方案5】:

编辑:Cactus 现在是一个死项目:http://attic.apache.org/projects/jakarta-cactus.html


你可能想看看仙人掌。

http://jakarta.apache.org/cactus/

项目描述

Cactus 是一个简单的测试框架,用于对服务器端 Java 代码(Servlet、EJB、标签库、过滤器等)进行单元测试。

Cactus 的目的是降低为服务器端代码编写测试的成本。它使用 JUnit 并对其进行扩展。

Cactus 实现了容器内策略,这意味着测试在容器内执行。

【讨论】:

【参考方案6】:

另一种方法是创建一个嵌入式服务器来“托管”您的 servlet,允许您使用旨在调用实际服务器的库来编写针对它的调用(这种方法的实用性在某种程度上取决于您可以轻松地制作“合法的”对服务器的程序调用——我正在测试一个 JMS(Java 消息服务)访问点,客户端比比皆是)。

您可以走几条不同的路线 - 通常的两条是 tomcat 和 jetty。

警告:选择要嵌入的服务器时要注意的是您正在使用的 servlet-api 版本(提供 HttpServletRequest 等类的库)。如果您使用的是 2.5,我发现 Jetty 6.x 运行良好(这是我将在下面给出的示例)。如果您使用的是 servlet-api 3.0,则嵌入 tomcat-7 的东西似乎是一个不错的选择,但是我不得不放弃使用它的尝试,因为我正在测试的应用程序使用的是 servlet-api 2.5。在尝试配置或启动服务器时,尝试将两者混合会导致 NoSuchMethod 和其他此类异常。

您可以像这样设置这样的服务器(Jetty 6.1.26,servlet-api 2.5):

public void startServer(int port, Servlet yourServletInstance)
    Server server = new Server(port);
    Context root = new Context(server, "/", Context.SESSIONS);

    root.addServlet(new ServletHolder(yourServletInstance), "/servlet/context/path");

    //If you need the servlet context for anything, such as spring wiring, you coudl get it like this
    //ServletContext servletContext = root.getServletContext();

    server.start();

【讨论】:

另外,如果你选择调查依赖注入,你可能会遇到 Spring。 Spring 使用上下文来查找注入的项目。如果您的 servlet 最终使用 spring,您可以通过将以下内容添加到上述方法(在 start 调用之前)为它提供与测试相同的上下文: XmlWebApplicationContext wctx = new XmlWebApplicationContext(); wctx.setParent(yourAppContext); wctx.setConfigLocation(""); wctx.setServletContext(servletContext); wctx.refresh(); servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, wctx);【参考方案7】:

使用Selenium 进行基于网络的单元测试。有一个名为 Selenium IDE 的 Firefox 插件,它可以记录网页上的操作并导出到使用 Selenium RC 运行测试服务器的 JUnit 测试用例。

【讨论】:

感谢这看起来不错,但它并没有真正测试方法/servlet 代码,不是直接吗?还是我错了。 确实如此,通过以编程方式触发 HTTP 请求。【参考方案8】:
 public class WishServletTest 
 WishServlet wishServlet;
 HttpServletRequest mockhttpServletRequest;
 HttpServletResponse mockhttpServletResponse;

@Before
public void setUp()
    wishServlet=new WishServlet();
    mockhttpServletRequest=createNiceMock(HttpServletRequest.class);
    mockhttpServletResponse=createNiceMock(HttpServletResponse.class);


@Test
public void testService()throws Exception
    File file= new File("Sample.txt");
    File.createTempFile("ashok","txt");
    expect(mockhttpServletRequest.getParameter("username")).andReturn("ashok");
    expect(mockhttpServletResponse.getWriter()).andReturn(new PrintWriter(file));
    replay(mockhttpServletRequest);
    replay(mockhttpServletResponse);
    wishServlet.doGet(mockhttpServletRequest, mockhttpServletResponse);
    FileReader fileReader=new FileReader(file);
    int count = 0;
    String str = "";
    while ( (count=fileReader.read())!=-1)
        str=str+(char)count;
    

    Assert.assertTrue(str.trim().equals("Helloashok"));
    verify(mockhttpServletRequest);
    verify(mockhttpServletResponse);




【讨论】:

【参考方案9】:

首先,您可能应该稍微重构一下,以便不在 doPost 代码中创建 DataManager。您应该尝试依赖注入来获取实例。 (有关 DI 的精彩介绍,请参见 Guice 视频。)。如果您被告知要开始对所有内容进行单元测试,那么 DI 是必不可少的。

注入依赖项后,您可以单独测试您的类。

要实际测试 servlet,还有其他较早的线程已经讨论过这个问题。尝试here 和here。

【讨论】:

好的,感谢您的 cmets,您是说 DataManager 应该在该 servlet 的方法中创建吗?我看了那个视频,并没有真正理解它:(对java很陌生,从未做过任何测试。 看一看 Guice 视频(至少是开头)——它很好地解释了为什么你不想在你计划进行单元测试的类中实例化一个新对象。跨度>

以上是关于如何使用 JUnit 测试我的 servlet的主要内容,如果未能解决你的问题,请参考以下文章

在 Servlet 和 JDBC 中为 JUnit 分离 H2 数据库 [关闭]

如何通过 Gradle 测试任务在我的 JUnit 上启用调试

如何使用 Gson 解析验证我的 Junit 测试

如果使用 Spring 和 Junit 4,如何编写内部测试类?或者我还能如何构建我的测试?

如何使用 Junit 5 使用 Testcontainer 对 Kafka 运行集成测试

如何使用 JUnit 测试异步进程