Mockito @Mock 不使用 when() 存根方法

Mockito @Mock does not stub method using when()

我正在开发一个简单的 REST API,具有 Spring 引导和六边形架构,用于基于著名视频游戏的个人项目。几天前,我使用 JUnit 5 (jupiter) 和 mockito 编写了单元测试。它们运行良好,但今天我更新了我的测试代码,因为我想为我的 API 添加 i18n 支持,以及一个允许禁用玩家与其官方 Minecraft UUID 之间的 linking 的简单配置。

我的 class 的构造函数已更改为添加一个 MessageSource 对象以支持 i18n 并为 uuid link 状态添加一个布尔值。我的 class 现在看起来像这样:

public class PlayerService implements PlayerApi {

    private final boolean ONLINE_MODE;
    private final PlayerSpi playerSpi;
    private final MojangApi mojangApi;
    private final MessageSource messageSource;

    public PlayerService(PlayerSpi playerSpi, MojangApi mojangApi,
                         boolean onlineMode, MessageSource messageSource) {
        this.playerSpi = playerSpi;
        this.mojangApi = mojangApi;
        this.ONLINE_MODE = onlineMode;
        this.messageSource = messageSource;
    }

    @Override
    public Option<Player> findByPseudoOrEmailAddress(String pseudo, String emailAddress) {
        if (pseudo.isEmpty() || emailAddress.isEmpty()) return Option.none();//TODO BETTER ERROR GEST USING EITHER ?
        return playerSpi.findByPseudoOrEmailAddress(pseudo, emailAddress);
    }

    @Override
    public Option<Player> findById(Long id) {
        return playerSpi.findById(id);
    }

    @Override
    public Boolean existByPseudo(String pseudo) {
        return playerSpi.existByPseudo(pseudo);
    }

    @Override
    public Boolean existByEmailAddress(String emailAddress) {
        return playerSpi.existByEmailAddress(emailAddress);
    }

    @Override
    public void save(Player player) {
        playerSpi.save(player);
    }

    @Override
    public Response<?> register(Player player) {

        //LINK PLAYER TO HIS MINECRAFT UUID IF ONLINE MODE IS TRUE
        if (ONLINE_MODE) {
            Either<MinecraftUUIDException, String> result = mojangApi.getPlayerUUID(player.getPseudo());
            if (result.isRight())
                player.setMinecraftUUID(result.get());
            else
                return Response.Builder.create()
                    .success(false)
                    .message(messageSource.getMessage("PlayerService.register.uuid-linking-failed",
                        null, LocaleContextHolder.getLocale()))
                    .body(result.getLeft())
                    .build();

        }
        save(player);
        return Response.Builder.create()
            .success(true)
            .message(messageSource.getMessage("PlayerService.register.success",
                null, LocaleContextHolder.getLocale()))
            .body(null)
            .build();
    }
}

所以我创建了一个测试 class,如下所示:

@ExtendWith(MockitoExtension.class)
@TestInstance(Lifecycle.PER_CLASS)
public class PlayerServiceTest {

    @Mock
    private PlayerSpi playerSpi;
    @Mock
    private MojangApi mojangApi;
    @Mock
    private MessageSource messageSource;
    private PlayerService playerService;

    @BeforeAll
    void setUp() {
        initMocks(this);
        playerService = new PlayerService(
            playerSpi,
            mojangApi,
            true,
            messageSource
        );
    }

    @Test
    void register_should_register() {
        Player p = new Player(
            1L,
            "Bad_Pop",
            "test@test.com",
            "password",
            List.empty(),
            "uuid"
        );

        when(mojangApi.getPlayerUUID(any())).thenReturn(Either.right("uuid"));
        ArgumentCaptor<Player> valueCapture = ArgumentCaptor.forClass(Player.class);
        lenient().doNothing().when(playerSpi).save(valueCapture.capture());

        Response<?> res = playerService.register(p);

        assertEquals(p.getId(), valueCapture.getValue().getId());
        assertEquals(p.getPseudo(), valueCapture.getValue().getPseudo());
        assertEquals(p.getEmailAddress(), valueCapture.getValue().getEmailAddress());
        assertEquals(p.getPassword(), valueCapture.getValue().getPassword());
        assertEquals(p.getRoles(), valueCapture.getValue().getRoles());
        assertEquals(p.getMinecraftUUID(), valueCapture.getValue().getMinecraftUUID());

        assertTrue(res.getSuccess());
        assertTrue("Player successfully registered.".equalsIgnoreCase(res.getMessage()));
        assertNull(res.getBody());
    }

    @Test
    void register_should_not_register() {
        Player p = new Player(
            1L,
            "Bad_Pop",
            "test@test.com",
            "password",
            List.empty(),
            "uuid"
        );

        when(mojangApi.getPlayerUUID(any())).thenReturn(
            Either.left(
                new MinecraftUUIDException("Unable to retrieve Minecraft UUID for this player. Please make sure this player name exists.")
            )
        );

        Response<?> res = playerService.register(p);

        assertFalse(res.getSuccess());
        assertTrue("An error occured while registering this player".equalsIgnoreCase(res.getMessage()));
        assertTrue(res.getBody().getClass().getSimpleName().equalsIgnoreCase("MinecraftUUIDException"));
        MinecraftUUIDException e = (MinecraftUUIDException) res.getBody();
        assertEquals("Unable to retrieve Minecraft UUID for this player. Please make sure this player name exists.",
            e.getMessage());
    }

//remaining tests ommitted
}

所以当我 运行 我的测试时,我每次都会遇到这种错误:

java.lang.NullPointerException
    at io.denoria.domain.core.service.PlayerService.register(PlayerService.java:61)
    at io.denoria.domain.PlayerServiceTest.register_should_register(PlayerServiceTest.java:178)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:567)
    at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:675)
    at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:125)
    at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:139)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:131)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:81)
    at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod[=12=](ExecutableInvoker.java:115)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke[=12=](ExecutableInvoker.java:105)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:104)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:62)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:43)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:35)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod(TestMethodTestDescriptor.java:202)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:198)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:135)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:69)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:135)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:125)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:123)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:125)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:123)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:125)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:123)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:248)
    at org.junit.platform.launcher.core.DefaultLauncher.lambda$execute(DefaultLauncher.java:211)
    at org.junit.platform.launcher.core.DefaultLauncher.withInterceptedStreams(DefaultLauncher.java:226)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:199)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:132)
    at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:69)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
    at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
    at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)
    Suppressed: org.mockito.exceptions.misusing.UnnecessaryStubbingException: 
Unnecessary stubbings detected.
Clean & maintainable test code requires zero unnecessary code.
Following stubbings are unnecessary (click to navigate to relevant line of code):
  1. -> at io.denoria.domain.PlayerServiceTest.register_should_register(PlayerServiceTest.java:174)
Please remove unnecessary stubbings or use 'lenient' strictness. More info: javadoc for UnnecessaryStubbingException class.
        at org.mockito.junit.jupiter.MockitoExtension.afterEach(MockitoExtension.java:230)
        at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeAfterEachCallbacks(TestMethodTestDescriptor.java:245)
        at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeAllAfterMethodsOrCallbacks(TestMethodTestDescriptor.java:256)
        at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
        at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeAllAfterMethodsOrCallbacks(TestMethodTestDescriptor.java:256)
        at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
        at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeAllAfterMethodsOrCallbacks(TestMethodTestDescriptor.java:255)
        at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeAfterEachCallbacks(TestMethodTestDescriptor.java:244)
        at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:141)
        ... 41 more

这让我相信模拟没有被 Mockito 执行。

我在其他 classes 中添加了对 i18n 的支持,我没有遇到这个问题。

initMocks() 应该在每个测试实例 class 中调用,因此应该在 @BeforeEach 方法中调用,而不是在 @BeforeAll 方法中调用。此外,@BeforeAll 需要一个静态方法 - 除非你切换了生命周期,但你的代码没有显示。

我想知道为什么 Jupiter 不抱怨非静态方法。也许您正在使用 JUnit 4 中的 @Test?如果您无法通过 @BeforeEach 解决问题,请在测试 class 中显示您所有的导入。