
本文旨在解决spring mvc控制器单元测试中常见的`nullpointerexception`问题,该问题通常在使用`@webmvctest`时因服务依赖未正确模拟和注入而导致。我们将深入探讨`@mockbean`注解的正确用法,以及它如何与`mockmvc`协同工作,确保控制器能够访问到其模拟的服务依赖,从而实现健壮且高效的web层测试。
Spring MVC控制器测试中的常见挑战
在Spring Boot项目中,当我们使用@WebMvcTest对控制器(Controller)进行单元测试时,一个常见的需求是隔离控制器,使其不依赖于实际的业务逻辑层(Service)和数据访问层(Repository)。这时,我们会选择使用Mocking框架(如Mockito)来模拟这些依赖。然而,如果模拟的方式不正确,很容易遇到java.lang.NullPointerException,指示控制器内部调用的服务对象为null。
例如,考虑以下一个典型的测试场景:
@WebMvcTest // 专注于Web层测试
@ContextConfiguration(classes = { TestAppContext.class }) // 指定测试上下文配置
class BillEntryControllerTest {
@Autowired
private BillEntryService billEntryService; // 尝试注入服务
@Autowired
private MockMvc mockMvc; // 自动注入MockMvc
@BeforeEach
public void setup() {
// 尝试手动设置MockMvc,这在@WebMvcTest下是不必要的
this.mockMvc = MockMvcBuilders.standaloneSetup(new BillEntryController())
.build();
}
@Test
public void checkUpdateBill() throws Exception {
// 尝试在测试方法内部模拟服务
billEntryService = Mockito.mock(BillEntryServiceImpl.class);
// 定义模拟行为
doNothing().when(billEntryService).addOrUpdateBill(any(BillEntry.class));
this.mockMvc
.perform(MockMvcRequestBuilders.post("/bill-entry/saveBillEntry").accept(MediaType.TEXT_HTML)
.param("amount", "10.0")
// ... 其他参数
)
.andExpect(model().errorCount(0)).andExpect(status().isOk());
}
}以及对应的控制器方法:
@PostMapping("/saveBillEntry")
public String saveBillEntry(Model model, @Valid @ModelAttribute("billEntry") BillEntryFormDto dto,
BindingResult theBindingResult) {
// ... 其他逻辑
// failing at here (line 157)
billEntryService.addOrUpdateBill(billEntry); // 这里抛出NullPointerException
return "redirect:"+ dto.getRedirect();
}上述测试代码的问题在于,尽管在测试类中声明了@Autowired private BillEntryService billEntryService;,并在@Test方法中使用了Mockito.mock()创建了一个模拟对象,但这个模拟对象并没有被注入到BillEntryController实例中。@WebMvcTest会启动一个有限的Spring应用上下文,并尝试注入控制器及其依赖。如果依赖是一个Spring管理的Bean,并且我们想用Mock替代它,仅仅在测试类中创建一个Mockito.mock()实例是不足以让Spring容器知道并将其注入到控制器中的。因此,控制器内部的billEntryService字段仍然是null,导致调用时出现NullPointerException。
此外,手动设置MockMvc(如在@BeforeEach中使用MockMvcBuilders.standaloneSetup())也是不必要的,因为@WebMvcTest会自动配置并注入一个功能完备的MockMvc实例。
解决方案:利用@MockBean进行依赖注入
Spring Boot提供了一个专门用于测试的注解@MockBean,它能够优雅地解决上述问题。@MockBean的作用是在Spring应用上下文中为指定的类型创建一个Mockito模拟对象,并将其注入到所有需要该类型依赖的Bean中。这意味着,当我们使用@WebMvcTest测试控制器时,@MockBean会自动将模拟的服务实例注入到控制器中。
以下是使用@MockBean修正后的测试代码:
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doNothing;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
// 假设TestAppContext包含BillEntryController的配置
// 或者如果BillEntryController是@Controller注解的,@WebMvcTest会自动扫描到
@WebMvcTest(BillEntryController.class) // 指定要测试的控制器,更精确
@Import(TestAppContext.class) // 如果TestAppContext包含必要的Bean定义,可以使用@Import
// @Transactional 在@WebMvcTest中通常不是必需的,因为它不涉及数据库事务
class BillEntryControllerTest {
@MockBean // 使用@MockBean来创建并注入BillEntryService的模拟对象
private BillEntryService billEntryService;
@Autowired // @WebMvcTest会自动配置并注入MockMvc
private MockMvc mockMvc;
// 移除@BeforeEach方法,因为MockMvc已由@WebMvcTest自动配置和注入
// 移除在测试方法中手动创建Mockito.mock()的调用
@Test
public void checkUpdateBill() throws Exception {
// 定义模拟对象的行为。此时billEntryService已经是@MockBean创建的模拟对象
doNothing().when(billEntryService).addOrUpdateBill(any(BillEntry.class));
this.mockMvc
.perform(MockMvcRequestBuilders.post("/bill-entry/saveBillEntry")
.accept(MediaType.TEXT_HTML)
.param("amount", "10.0")
.param("owner", "User")
.param("property", "Prop")
.param("receiptNumber", "ABC")
.param("accountName", "AC")
.param("billerName", "BN")
.param("datePaid", "20/10/2022")
.param("dateDue", "20/10/2022"))
.andExpect(model().errorCount(0))
.andExpect(status().isOk());
}
}关键调整和最佳实践
-
@MockBean的正确使用:
- 将@Autowired private BillEntryService billEntryService;替换为@MockBean private BillEntryService billEntryService;。
- @MockBean会自动在Spring上下文中注册一个BillEntryService的Mock实例,并将其注入到BillEntryController中。
- 不再需要在测试方法内部手动调用Mockito.mock(BillEntryServiceImpl.class)。
-
MockMvc的自动配置:
- @WebMvcTest注解会自动配置MockMvc,并将其注入到测试类中。因此,@BeforeEach方法中手动创建MockMvc实例(如MockMvcBuilders.standaloneSetup(...))是多余的,应该被移除。
-
@WebMvcTest的精确性:
- 为了提高测试的效率和隔离性,建议在@WebMvcTest注解中明确指定要测试的控制器类,例如@WebMvcTest(BillEntryController.class)。这样可以进一步限制Spring上下文加载的Bean数量。
-
@ContextConfiguration或@Import:
- 如果控制器依赖的Bean(例如BillEntryController自身)需要特定的配置或组件扫描才能被@WebMvcTest发现和加载,可以使用@ContextConfiguration或@Import来引入必要的配置类。在大多数情况下,@WebMvcTest会自动扫描控制器。
-
模拟行为的定义:
- 一旦@MockBean创建了模拟对象,就可以像使用任何Mockito模拟对象一样,通过when().then...()或do...().when()来定义其行为。
-
@Transactional的适用性:
- 在@WebMvcTest这种专注于Web层的测试中,通常不需要@Transactional注解,因为这类测试通常不涉及实际的数据库操作。如果测试确实需要事务管理,例如在集成测试中,才应考虑使用。
总结
通过正确地使用Spring Boot提供的@MockBean注解,我们可以有效地在@WebMvcTest环境下模拟控制器所依赖的服务,从而避免NullPointerException并实现真正的单元测试。@MockBean简化了Mock对象的创建和注入过程,使其与Spring的依赖注入机制无缝集成。同时,依赖@WebMvcTest自动配置的MockMvc实例,可以避免不必要的MockMvc手动设置,使测试代码更加简洁和符合惯例。掌握这些最佳实践,将有助于构建更健壮、可维护的Spring MVC应用程序测试套件。










