如何用JUnit测试Servlet的void类型doGet/doPost方法并解决空指针异常?
咱们先来看你遇到的核心问题:堆栈跟踪显示空指针异常来自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()方法触发NPEgetActionString(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的最佳实践
- 抽离业务逻辑:尽量把
Controller.getMail()这类业务逻辑放到独立的Service层,Servlet只负责参数解析和请求转发,这样Service层的测试会更简单,Servlet测试只需要验证转发和参数传递逻辑。 - 避免静态依赖:如果可以,重构
SecurityTool这类静态工具类为非静态,通过依赖注入的方式引入,这样测试时不需要mock静态方法,更符合面向对象设计。 - 聚焦Servlet职责:Servlet的核心职责是处理请求和转发,测试时重点验证参数映射、属性设置、转发路径是否正确,而不是业务逻辑的结果(业务逻辑交给Service层测试)。
内容的提问来源于stack exchange,提问作者Nijnhoofd

