如何处理 Mock 和 InjectMock

How to deal with Mock and InjectMock

假设我有这个class(为简洁起见省略了一些部分):

package com.bitbank.config;

@Component
public class JwtTokenUtil implements Serializable {
    
    private static final long serialVersionUID = -2338626292552177485L;

    public static final long JWT_TOKEN_VALIDITY = 5L * 60 * 60;
    
    @Value("${jwt.secret}")
    private String secret;
    
    @Autowired
    private TimeSource timeSource;

    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        return doGenerateToken(claims, userDetails.getUsername());
    }

    // while creating the token -
    // 1. Define claims of the token, like Issuer, Expiration, Subject, and the ID
    // 2. Sign the JWT using the HS512 algorithm and secret key.
    // 3. According to JWS Compact
    // Serialization(https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#section-3.1)
    // compaction of the JWT to a URL-safe string
    private String doGenerateToken(Map<String, Object> claims, String subject) {

        return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(timeSource.getCurrentTimeMillis()))
                .setExpiration(new Date(timeSource.getCurrentTimeMillis() + JWT_TOKEN_VALIDITY * 1000))
                .signWith(SignatureAlgorithm.HS512, secret).compact();
    }

}

为了测试 Token 是否过期,我创建了另一个 class:

package com.bitbank.utils;

import org.springframework.stereotype.Component;

@Component
public class TimeSource {
    
    public Long getCurrentTimeMillis() {
        return System.currentTimeMillis();
    }
    
}

所以,我可以注入假的/旧的 millis 并测试令牌是否已过期。

这是我的 class 测试:

@SpringBootTest
@TestPropertySource("classpath:application.properties")
class JwtTokenUtilTest {
    
    @Mock
    private TimeSource timeSource;
    
    @InjectMocks
    private JwtTokenUtil jwtTokenUtil;
    
    @Test
    void canCheckIsExpired() {
        
        when(timeSource.getCurrentTimeMillis()).thenReturn(100L);
        UserDetails userDetails = new User("username", "password", new ArrayList<>());
        String token = jwtTokenUtil.generateToken(userDetails);
        
        assertFalse(jwtTokenUtil.validateToken(token, userDetails));
        
    }
    
}

但我有几个问题。

使用这种模式(@Mock 和@InjectMocks)我有错误:

base64-encoded secret key cannot be null or empty.`

我知道因为 class 本身是 Mocked,所以无法读取测试文件夹中的 application.properties。

如果我使用 @Autowired:

@SpringBootTest
@TestPropertySource("classpath:application.properties")
class JwtTokenUtilTest {
    
    @Mock
    private TimeSource timeSource;
    
    @Autowired
    private JwtTokenUtil jwtTokenUtil;

测试本身失败。当然,好像没注入100L的millis

如果我交换 class:

@Autowired
private TimeSource timeSource;

@InjectMocks
private JwtTokenUtil jwtTokenUtil;

我得到:

Cannot invoke "com.bitbank.utils.TimeSource.getCurrentTimeMillis()" because "this.timeSource" is null

如果你真的想为此使用 @SpringBootTest 那么你需要自动装配 JwtTokenUtil 并使用 @MockBean (**not** @Mock) for the TimeSource`。

@SpringBootTest
@TestPropertySource("classpath:application.properties")
class JwtTokenUtilTest {
    
    @MockBean
    private TimeSource timeSource;
    
    @Autowired
    private JwtTokenUtil jwtTokenUtil;

@MockBean 是一个 Spring 引导注解(利用 Mockito)来指示对于 TimeSource 需要创建一个 mock 并将其用作依赖注入的 bean。这是为了将它与 @SpringBootTest 或来自 Spring 的其他测试注释之一一起使用 Boot like @DataJpaTest@WebMvcTest

@Mock 是普通的 Mockito 注释,用于在使用 Mockito 进行单元测试时使用。这与 Mockito runner 或 JUnit 的手动初始化模拟的扩展一起使用,但不与 Spring 启动测试注释一起使用(因为它们很高兴地不知道如何处理 @Mock)。