JUnit5 身份验证测试不一致

问题描述 投票:0回答:1

问题

您好! 我一直在对我一直在从事的 Spring Boot Restful 实践项目进行单元测试,没有任何问题,但是当我开始测试身份验证业务逻辑时,一些测试会在第一次运行时通过,但在下一次运行时不会通过一,反之亦然。

可能的原因#1

我一直在测试的逻辑包括

Java Mail Sender
(用于电子邮件确认或忘记密码请求),该逻辑背后的方法用
@Transactional
@Async
进行注释。我相信问题是由于测试不是异步的,但是我尝试在测试中修改
@Transactional
@Async
注释,但我没有成功。

可能的原因#2

此问题的另一个原因可能是我没有重置我的模拟,但是当我尝试使用拆解和

@AfterEach
重置模拟实例时,它根本没有帮助,或者可能我错误地使用了它。

测试结果

Successfull run

Reruned same tests few seconds after the previous run

代码示例

@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);
    }
}

堆栈跟踪 shouldResetPassword()

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)

堆栈跟踪 shouldConfrimEmail()

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.

java spring spring-boot authentication junit5
1个回答
0
投票

我告诉过您包含您的来源,并且您会得到很快的回复,因为这是一个常见问题。我多次查看了您的代码,但找不到任何看起来不对劲的地方,所以我承认这不是一个简单的错误,但希望这个答案将来能对您有所帮助。

在你的例子中,事实证明这是导致问题的一些因素的组合,那就是使用 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 正在选择要注入的参数名称。在某些情况下它会得到正确的结果,而在其他情况下它只是将相同的模拟实例注入到两个变量中。以下是解决此问题的三种方法。

  1. 删除 AllArgsConstructor 注释并定义您自己的构造函数,从而保留参数的名称。

      public AuthenticationService(UserRepository userRepository, TokenService emailConfirmationTokenService, TokenService resetPasswordTokenService, PasswordEncoder encoder) { ... }
    
  2. 删除 AllArgsConstructor 注释,从依赖项中删除最终运算符,并使用 @Setter 注释您的服务以使用 setter 注入。 @服务 @塞特 公共类身份验证服务{ 私有 UserRepository 用户存储库; 私人 TokenService emailConfirmationTokenService; 私有TokenService重置PasswordTokenService; 私有PasswordEncoder编码器; }

  3. 使用单个模拟来定义 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);
         }
     }
    
© www.soinside.com 2019 - 2024. All rights reserved.