您好! 我一直在对我一直在从事的 Spring Boot Restful 实践项目进行单元测试,没有任何问题,但是当我开始测试身份验证业务逻辑时,一些测试会在第一次运行时通过,但在下一次运行时不会通过一,反之亦然。
我一直在测试的逻辑包括
Java Mail Sender
(用于电子邮件确认或忘记密码请求),该逻辑背后的方法用 @Transactional
和 @Async
进行注释。我相信问题是由于测试不是异步的,但是我尝试在测试中修改 @Transactional
和 @Async
注释,但我没有成功。
此问题的另一个原因可能是我没有重置我的模拟,但是当我尝试使用拆解和
@AfterEach
重置模拟实例时,它根本没有帮助,或者可能我错误地使用了它。
@Service
@AllArgsConstructor
public class AuthenticationService {
private final UserRepository userRepository;
//TokenService instances are @Qualifier
private final TokenService emailConfirmationTokenService;
private final TokenService resetPasswordTokenService;
private final PasswordEncoder encoder;
@Transactional
public void confirmEmail(String value) {
emailConfirmationTokenService.confirmToken(value);
}
@Transactional
public void resetPassword(String value, ResetPasswordRequestDTO requestDTO) {
User user = resetPasswordTokenService.getUserByToken(value);
resetPasswordTokenService.confirmToken(value);
user.setPassword(encoder.encode(requestDTO.newPassword()));
userRepository.save(user);
}
}
@ExtendWith(MockitoExtension.class)
public class AuthenticationServiceUnitTest {
@Mock
private UserRepository userRepository;
@Mock
private Properties properties;
@Mock
private User user;
@Mock
private AuthenticationHelper authenticationHelper;
@Mock
private PasswordEncoder passwordEncoder;
@Mock
private EmailService emailService;
@Mock
private ResetPasswordTokenService resetPasswordTokenService;
@Mock
private EmailConfirmationTokenService emailConfirmationTokenService;
@InjectMocks
private AuthenticationService authenticationService;
@AfterEach
void tearDown() {
reset(userRepository, passwordEncoder, emailService,
emailConfirmationTokenService, resetPasswordTokenService,passwordEncoder,
authenticationHelper, properties);
}
@Test
void shouldSendResetPasswordEmail() throws MessagingException {
String recipient = "[email protected]";
User userRequesting = new User();
userRequesting.setEmail(recipient);
String resetURL = "reset-url";
String token = "generated token";
String fullURL = "reset-urlgenerated token";
ForgotPasswordRequestDTO requestDTO = new ForgotPasswordRequestDTO(recipient);
when(userRepository.findUserByEmailIgnoreCase(recipient)).thenReturn(Optional.of(userRequesting));
when(properties.getResetURL()).thenReturn(resetURL);
when(resetPasswordTokenService.generateToken(userRequesting)).thenReturn(token);
authenticationService.forgotPassword(requestDTO);
System.out.println(fullURL);
verify(emailService).sendResetPasswordEmail(userRequesting, fullURL);
}
@Test
void shouldThrowResourceNotFoundWhenProvidedInvalidEmailForForgotPassword() {
String recipient = "[email protected]";
ForgotPasswordRequestDTO requestDTO = new ForgotPasswordRequestDTO(recipient);
when(userRepository.findUserByEmailIgnoreCase(recipient)).thenThrow(ResourceNotFoundException.class);
assertThrows(ResourceNotFoundException.class, () -> authenticationService.forgotPassword(requestDTO));
}
@Test
void shouldResetPassword() {
String tokenValue = "validToken";
String newPassword = "newSecurePassword";
User user = new User();
when(resetPasswordTokenService.getUserByToken(tokenValue)).thenReturn(user);
doNothing().when(resetPasswordTokenService).confirmToken(tokenValue);
when(passwordEncoder.encode(newPassword)).thenReturn(newPassword); // Mock password encoding
authenticationService.resetPassword(tokenValue, new ResetPasswordRequestDTO(newPassword, newPassword));
verify(userRepository).save(user);
assertThat(user.getPassword()).isEqualTo(newPassword);
}
@Test
void shouldConfirmEmail() {
String value = "email confirmation";
authenticationService.confirmEmail(value);
verify((emailConfirmationTokenService)).confirmToken(value);
}
}
java.lang.NullPointerException: Cannot invoke "com.cranker.cranker.user.User.setPassword(String)" because "user" is null
at com.cranker.cranker.authentication.AuthenticationService.resetPassword(AuthenticationService.java:82)
at com.cranker.cranker.unit.authentication.AuthenticationServiceUnitTest.shouldResetPassword(AuthenticationServiceUnitTest.java:97)
at java.base/java.lang.reflect.Method.invoke(Method.java:577)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
Wanted but not invoked:
emailConfirmationTokenService.confirmToken(
"email confirmation"
);
-> at com.cranker.cranker.token.impl.EmailConfirmationTokenService.confirmToken(EmailConfirmationTokenService.java:30)
Actually, there were zero interactions with this mock.
Wanted but not invoked:
emailConfirmationTokenService.confirmToken(
"email confirmation"
);
-> at com.cranker.cranker.token.impl.EmailConfirmationTokenService.confirmToken(EmailConfirmationTokenService.java:30)
Actually, there were zero interactions with this mock.
我告诉过您包含您的来源,并且您会得到很快的回复,因为这是一个常见问题。我多次查看了您的代码,但找不到任何看起来不对劲的地方,所以我承认这不是一个简单的错误,但希望这个答案将来能对您有所帮助。
在你的例子中,事实证明这是导致问题的一些因素的组合,那就是使用 Lombok 自动生成你的构造函数,并使用mockito 自动注入你的模拟。以下是场景。
首先,让我们创建一个具有四个相同类型的依赖项的简单类。然后,让我们创建一个对它们中的每个执行一些操作的方法。
@Service
@AllArgsConstructor
public class AuthenticationService {
private final PasswordEncoder firstPasswordEncoder;
private final PasswordEncoder secondPasswordEncoder;
private final PasswordEncoder thirdPasswordEncoder;
private final PasswordEncoder fourthPasswordEncoder;
public String encode(String value) {
String firstPass = firstPasswordEncoder.encode(value);
String secondPass = secondPasswordEncoder.encode(firstPass);
String thirdPass = thirdPasswordEncoder.encode(secondPass);
return fourthPasswordEncoder.encode(thirdPass);
}
}
我们的测试也很简单,看起来像这样:
@ExtendWith(MockitoExtension.class)
class AuthenticationServiceTest {
@Mock
private PasswordEncoder firstPasswordEncoder;
@Mock
private PasswordEncoder secondPasswordEncoder;
@Mock
private PasswordEncoder thirdPasswordEncoder;
@Mock
private PasswordEncoder fourthPasswordEncoder;
@InjectMocks
private AuthenticationService authenticationService;
@Test
public void testEncoding() {
Constructor<?>[] constructors = AuthenticationService.class.getConstructors();
String password = "password";
when(firstPasswordEncoder.encode(password)).thenReturn("passwordTwo");
when(secondPasswordEncoder.encode("passwordTwo")).thenReturn("passwordThree");
when(thirdPasswordEncoder.encode("passwordThree")).thenReturn("password4");
when(fourthPasswordEncoder.encode("password4")).thenReturn("Made it");
String encoded = authenticationService.encode(password);
assertThat(encoded, is(equalTo("Made it")));
}
}
就像您的测试一样,此测试失败并显示
java.lang.AssertionError:
Expected: is "Made it"
but: was null
Expected :is "Made it"
Actual :null
这会失败,因为如果 Mockito 看到两个相同类型的构造函数参数,那么它将使用参数名称来确定要注入哪个模拟。您在测试中对模拟进行了适当的命名,但由于 AllArgsConstructor 删除了参数名称,因此 Mockito 正在选择要注入的参数名称。在某些情况下它会得到正确的结果,而在其他情况下它只是将相同的模拟实例注入到两个变量中。以下是解决此问题的三种方法。
删除 AllArgsConstructor 注释并定义您自己的构造函数,从而保留参数的名称。
public AuthenticationService(UserRepository userRepository, TokenService emailConfirmationTokenService, TokenService resetPasswordTokenService, PasswordEncoder encoder) { ... }
删除 AllArgsConstructor 注释,从依赖项中删除最终运算符,并使用 @Setter 注释您的服务以使用 setter 注入。 @服务 @塞特 公共类身份验证服务{ 私有 UserRepository 用户存储库; 私人 TokenService emailConfirmationTokenService; 私有TokenService重置PasswordTokenService; 私有PasswordEncoder编码器; }
使用单个模拟来定义 emailConfirmationTokenService 和 ResetPasswordTokenService 的行为。因为mockito 可以完美地多次注入同一个模拟,所以只需为两者定义一个模拟。
@ExtendWith(MockitoExtension.class)
public class AuthenticationServiceUnitTest {
@Mock
private UserRepository userRepository;
@Mock
private PasswordEncoder passwordEncoder;
@Mock
private ResetPasswordTokenService myOneAndOnlyTokenMockThatIsInjectedIntoTwoProperties;
@InjectMocks
private AuthenticationService authenticationService;
@Test
void shouldResetPassword() {
String tokenValue = "validToken";
String newPassword = "newSecurePassword";
User user = new User();
when(myOneAndOnlyTokenMockThatIsInjectedIntoTwoProperties.getUserByToken(tokenValue)).thenReturn(user);
when(passwordEncoder.encode(newPassword)).thenReturn(newPassword); // Mock password encoding
authenticationService.resetPassword(tokenValue, new ResetPasswordRequestDTO(newPassword, newPassword));
// I like to just capture the argument to mock and assert on the captured instance
ArgumentCaptor<User> userArgumentCaptor = ArgumentCaptor.forClass(User.class);
verify(userRepository).save(userArgumentCaptor.capture());
assertThat(userArgumentCaptor.getValue().getPassword(), is(equalTo("newSecurePassword")));
}
@Test
void shouldConfirmEmail() {
String value = "email confirmation";
authenticationService.confirmEmail(value);
verify((myOneAndOnlyTokenMockThatIsInjectedIntoTwoProperties)).confirmToken(value);
}
}