You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用JUnit测试Servlet的void类型doGet/doPost方法并解决空指针异常?

解决Servlet的JUnit测试空指针异常及测试方案优化

咱们先来看你遇到的核心问题:堆栈跟踪显示空指针异常来自SecurityTool.checkLogin方法的第49行,这说明这个工具类方法里调用了HttpServletRequest或HttpServletResponse的某个未被mock的方法,导致返回null触发了异常。另外你的测试代码还有几个容易踩坑的细节问题,我们一步步来解决:

一、先解决空指针异常的根源

SecurityTool.checkLogin是静态工具方法,直接调用它会让测试依赖于内部未被mock的逻辑。你可以用Mockito的mockStatic功能(需要Mockito 3.4+版本)直接mock这个方法的返回值,避免进入其内部逻辑触发NPE:

// 在测试方法中包裹需要mock静态方法的代码块
try (MockedStatic<SecurityTool> mockedSecurityTool = Mockito.mockStatic(SecurityTool.class)) {
    // 让checkLogin直接返回true,跳过登录验证逻辑
    mockedSecurityTool.when(() -> SecurityTool.checkLogin(any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
    
    // 这里执行你的doPost/doGet测试逻辑
}

如果SecurityTool内部依赖了request.getSession()或request.getAttribute("user")这类方法,你也可以针对性mock这些方法:

HttpSession mockSession = mock(HttpSession.class);
when(request.getSession()).thenReturn(mockSession);
when(mockSession.getAttribute("loggedUser")).thenReturn(new User()); // 构造一个测试用的用户对象

二、修正测试代码中的关键错误

1. 参数名大小写不匹配

你的Servlet中获取参数用的是request.getParameter("firstname")(小写n),但测试里mock的是request.getParameter("firstName")(大写N),这会导致firstName变量为null,直接触发错误校验逻辑跳回搜索页面,根本走不到业务代码。

修正为:

when(request.getParameter("firstname")).thenReturn("Piet");
when(request.getParameter("lastname")).thenReturn("Vries");
when(request.getParameter("companyname")).thenReturn("Spring");

2. 错误的断言逻辑

你的测试试图通过response.getWriter()的输出来断言结果,但doPost里用的是RequestDispatcher.forward()跳转JSP,这种情况下response的writer不会输出任何内容,所以这个断言永远会失败。

Servlet是请求转发/参数传递的角色,我们应该验证它的核心行为:

  • 是否正确设置了request属性
  • 是否转发到了正确的JSP路径
  • 业务逻辑是否被正确触发

3. 缺失的mock点

你还需要mock以下内容,避免后续逻辑触发异常:

  • request.getRequestURI():Servlet里用它设置originalURI属性
  • RequestDispatcher:避免forward()方法触发NPE
  • getActionString(request):这个方法的返回值决定了switch分支,你需要mock它依赖的request方法(比如如果它是从URI或request参数取action,就对应mock这些值)

三、完整的测试示例

下面是优化后的doPost测试代码,覆盖了成功搜索和参数校验失败两种场景:

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.MockitoJUnitRunner;
import javax.servlet.RequestDispatcher;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class SearchContactServletTest {

    @Mock
    private HttpServletRequest request;
    @Mock
    private HttpServletResponse response;
    @Mock
    private RequestDispatcher requestDispatcher;

    @Test
    public void testDoPost_SearchUser_Success() throws Exception {
        // 1. Mock正确的请求参数
        when(request.getParameter("firstname")).thenReturn("Piet");
        when(request.getParameter("prefix")).thenReturn("de");
        when(request.getParameter("lastname")).thenReturn("Vries");
        when(request.getParameter("domain")).thenReturn("springprofessional.nl");
        when(request.getParameter("companyname")).thenReturn("Spring");
        
        // 2. Mock action相关逻辑(假设getActionString从URI判断)
        when(request.getRequestURI()).thenReturn("/searchUser");
        
        // 3. Mock SecurityTool跳过登录验证
        try (MockedStatic<SecurityTool> mockedSecurityTool = mockStatic(SecurityTool.class)) {
            mockedSecurityTool.when(() -> SecurityTool.checkLogin(request, response)).thenReturn(true);
            
            // 4. Mock RequestDispatcher避免forward异常
            when(request.getRequestDispatcher(anyString())).thenReturn(requestDispatcher);
            doNothing().when(requestDispatcher).forward(request, response);
            
            // 5. Mock业务逻辑层,避免依赖真实服务
            PunchCard mockPunchCard = new PunchCard("Piet", "de", "Vries", "springprofessional.nl", "Spring");
            mockPunchCard.setSearchResults(new SearchResult[]{}); // 构造测试用的搜索结果
            try (MockedStatic<Controller> mockedController = mockStatic(Controller.class)) {
                mockedController.when(() -> Controller.getMail(any(PunchCard.class))).thenReturn(mockPunchCard);
                
                // 执行测试方法
                new SearchContactServlet().doPost(request, response);
                
                // 6. 验证核心行为是否正确
                verify(request).setAttribute("originalURI", "/searchUser");
                verify(request).setAttribute(eq("punchCard"), any(PunchCard.class));
                verify(request).setAttribute(eq("searchResults"), any(SearchResult[].class));
                // 验证转发路径正确
                verify(request).getRequestDispatcher("/WEB-INF/jsp/result.jsp");
                verify(requestDispatcher).forward(request, response);
                // 验证业务方法被正确调用
                mockedController.verify(() -> Controller.getMail(argThat(punchCard ->
                        punchCard.getFirstName().equals("Piet") &&
                        punchCard.getDomain().equals("springprofessional.nl")
                )));
            }
        }
    }

    @Test
    public void testDoPost_SearchUser_MissingFirstName() throws Exception {
        // 测试参数校验失败场景
        when(request.getParameter("firstname")).thenReturn(null);
        when(request.getParameter("lastname")).thenReturn("Vries");
        when(request.getParameter("domain")).thenReturn("springprofessional.nl");
        when(request.getRequestURI()).thenReturn("/searchUser");

        try (MockedStatic<SecurityTool> mockedSecurityTool = mockStatic(SecurityTool.class)) {
            mockedSecurityTool.when(() -> SecurityTool.checkLogin(request, response)).thenReturn(true);
            
            when(request.getRequestDispatcher(anyString())).thenReturn(requestDispatcher);
            doNothing().when(requestDispatcher).forward(request, response);
            
            new SearchContactServlet().doPost(request, response);
            
            // 验证错误信息被正确设置
            verify(request).setAttribute(eq("errors"), argThat(errors ->
                    errors.containsKey("firstname") && 
                    errors.get("firstname").equals("Oops, something went wrong. Please enter a first name.")
            ));
            // 验证跳回搜索页面
            verify(request).getRequestDispatcher("/WEB-INF/jsp/searchContact.jsp");
            verify(requestDispatcher).forward(request, response);
        }
    }
}

四、测试Servlet的最佳实践

  1. 抽离业务逻辑:尽量把Controller.getMail()这类业务逻辑放到独立的Service层,Servlet只负责参数解析和请求转发,这样Service层的测试会更简单,Servlet测试只需要验证转发和参数传递逻辑。
  2. 避免静态依赖:如果可以,重构SecurityTool这类静态工具类为非静态,通过依赖注入的方式引入,这样测试时不需要mock静态方法,更符合面向对象设计。
  3. 聚焦Servlet职责:Servlet的核心职责是处理请求和转发,测试时重点验证参数映射、属性设置、转发路径是否正确,而不是业务逻辑的结果(业务逻辑交给Service层测试)。

内容的提问来源于stack exchange,提问作者Nijnhoofd

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 04:25:41