如何用Mockito正确Mock Spring Boot Repository?测试失败求助
我是一名刚接触后端领域的前端工程师(FEE),正在用Spring Boot做练习项目。开发了一个AdminAuthService,通过调用AdminRepository存储的Admin对象用户名和密码,验证请求的账号密码是否合法。该服务用Postman测试正常,但JUnit/Mockito单元测试始终无法通过。首次使用JUnit/Mockito,推测是Repository的Mock方式有误。
在Service的validateIsAdmin方法中添加日志后发现:测试时Service使用的是Repository的真实用户名和密码,而非Mock的值,导致测试断言失败。目标是让Service使用Mock的账号密码进行校验。
Service日志代码
log.info("username and password for repository is: " + adminRepository.getAdminUserName() + " | " + adminRepository.getAdminPassword()); log.info("Recieved admin auth request with {}", adminRequest.getUsername() + " | " + adminRequest.getPassword());
Service类代码
package com.couvq.readinglist.service; import com.couvq.readinglist.dto.AdminRequest; import com.couvq.readinglist.dto.AdminResponse; import com.couvq.readinglist.repository.AdminRepository; import lombok.RequiredArgsConstructor; import lombok.log4j.Log4j2; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor @Log4j2 public class AdminAuthService { private final AdminRepository adminRepository; public AdminResponse validateIsAdmin(AdminRequest adminRequest) { log.info("username and password for repository is: " + adminRepository.getAdminUserName() + " | " + adminRepository.getAdminPassword()); log.info("Recieved admin auth request with {}", adminRequest.getUsername() + " | " + adminRequest.getPassword()); // if username and password of request matches that of repository, isAdmin is true if (adminRequest.getUsername().equals(adminRepository.getAdminUserName()) && adminRequest.getPassword().equals(adminRepository.getAdminPassword())) { return AdminResponse.builder() .isAdmin(true) .build(); } else { return AdminResponse.builder() .isAdmin(false) .build(); } } }
测试用例代码
import com.couvq.readinglist.dto.AdminRequest; import com.couvq.readinglist.dto.AdminResponse; import com.couvq.readinglist.repository.AdminRepository; import com.couvq.readinglist.service.AdminAuthService; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.when; @SpringBootTest @ExtendWith(MockitoExtension.class) class ReadingListApplicationTests { @Autowired private AdminAuthService adminAuthService; @Mock private AdminRepository adminRepository; @Test public void validateIsAdminWithAdminUserNameAndPasswordReturnsTrueResponse() { when(adminRepository.getAdminUserName()).thenReturn("username"); when(adminRepository.getAdminPassword()).thenReturn("password"); AdminRequest request = AdminRequest.builder() .username("username") .password("password") .build(); AdminResponse response = adminAuthService.validateIsAdmin(request); AdminResponse expectedResponse = AdminResponse.builder() .isAdmin(true) .build(); assertEquals(expectedResponse, response); } }
测试断言失败输出
org.opentest4j.AssertionFailedError: Expected :AdminResponse(isAdmin=true) Actual :AdminResponse(isAdmin=false)
错误根源
你当前的测试用了@SpringBootTest,这会启动完整的Spring应用上下文,@Autowired注入的AdminAuthService是容器内的真实实例,它依赖的AdminRepository也是真实Bean。而你用@Mock创建的AdminRepository是孤立的Mock对象,并没有替换掉容器中的真实实例,所以Service调用的还是真实Repository的方法,导致断言失败。
修复方案(两种可选)
方案1:使用@MockBean替换Spring容器中的Repository(集成测试场景)
把@Mock改成@MockBean,它会自动替换Spring上下文中对应的AdminRepository Bean,这样@Autowired的AdminAuthService就会使用Mock的Repository了。
修改后的测试代码:
import com.couvq.readinglist.dto.AdminRequest; import com.couvq.readinglist.dto.AdminResponse; import com.couvq.readinglist.repository.AdminRepository; import com.couvq.readinglist.service.AdminAuthService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.when; @SpringBootTest class ReadingListApplicationTests { @Autowired private AdminAuthService adminAuthService; @MockBean // 替换成@MockBean private AdminRepository adminRepository; @Test public void validateIsAdminWithAdminUserNameAndPasswordReturnsTrueResponse() { when(adminRepository.getAdminUserName()).thenReturn("username"); when(adminRepository.getAdminPassword()).thenReturn("password"); AdminRequest request = AdminRequest.builder() .username("username") .password("password") .build(); AdminResponse response = adminAuthService.validateIsAdmin(request); AdminResponse expectedResponse = AdminResponse.builder() .isAdmin(true) .build(); assertEquals(expectedResponse, response); } }
方案2:纯单元测试(不启动Spring上下文)
去掉@SpringBootTest和@Autowired,手动创建AdminAuthService实例并注入Mock的AdminRepository,这种方式更轻量,适合纯单元测试。
修改后的测试代码:
import com.couvq.readinglist.dto.AdminRequest; import com.couvq.readinglist.dto.AdminResponse; import com.couvq.readinglist.repository.AdminRepository; import com.couvq.readinglist.service.AdminAuthService; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) // 仅启动Mockito class AdminAuthServiceTest { // 建议将测试类名改为对应Service的测试类,更规范 @Mock private AdminRepository adminRepository; // 手动创建Service实例,注入Mock的Repository private AdminAuthService adminAuthService = new AdminAuthService(adminRepository); @Test public void validateIsAdminWithAdminUserNameAndPasswordReturnsTrueResponse() { when(adminRepository.getAdminUserName()).thenReturn("username"); when(adminRepository.getAdminPassword()).thenReturn("password"); AdminRequest request = AdminRequest.builder() .username("username") .password("password") .build(); AdminResponse response = adminAuthService.validateIsAdmin(request); AdminResponse expectedResponse = AdminResponse.builder() .isAdmin(true) .build(); assertEquals(expectedResponse, response); } }
额外注意
要确保AdminResponse类重写了equals()和hashCode()方法,因为assertEquals是通过这两个方法比较对象是否相等的。如果使用Lombok,可以给AdminResponse添加@EqualsAndHashCode注解,让Builder生成的实例能正确比较。
内容的提问来源于stack exchange,提问作者couvq

