使用Mockito 2模拟服务会导致存根错误。

52

我试图使用Mockito模拟一个类的行为。这在使用Mockito 1.x时可以工作。迁移到JUnit 5和Mockito 2后,似乎不再起作用。

@ExtendWith(MockitoExtension.class)
public class MockitoExample {

  static abstract class TestClass {
    public abstract int booleanMethod(boolean arg);
  }

  @Mock
  TestClass testClass;

  @BeforeEach
  public void beforeEach() {
    when(testClass.booleanMethod(eq(true))).thenReturn(1);
    when(testClass.booleanMethod(eq(false))).thenReturn(2);
  }

  @Test
  public void test() {
    assertEquals(1,testClass.booleanMethod(true));
    assertEquals(2,testClass.booleanMethod(false));
  }
}
期望的是,被mock的TestClass表现出测试方法中所测试的行为。
我得到的错误是:
org.mockito.exceptions.misusing.PotentialStubbingProblem: 

  Strict stubbing argument mismatch. Please check:
   - this invocation of 'booleanMethod' method:
      testClass.booleanMethod(false);
      -> at org.oneandone.ejbcdiunit.mockito_example.MockitoExample.beforeEach(MockitoExample.java:30)
   - has following stubbing(s) with different arguments:
      1. testClass.booleanMethod(false);
        -> at org.oneandone.ejbcdiunit.mockito_example.MockitoExample.beforeEach(MockitoExample.java:29)
  Typically, stubbing argument mismatch indicates user mistake when writing tests.
  Mockito fails early so that you can debug potential problem easily.
  However, there are legit scenarios when this exception generates false negative signal:
    - stubbing the same method multiple times using 'given().will()' or 'when().then()' API
      Please use 'will().given()' or 'doReturn().when()' API for stubbing.
    - stubbed method is intentionally invoked with different arguments by code under test
      Please use default or 'silent' JUnit Rule (equivalent of Strictness.LENIENT).
  For more information see javadoc for PotentialStubbingProblem class.

在这两种情况中,参数 false 似乎被匹配了,尽管我明显是与 true 匹配的。

这是 Mockito 2.17 的一个 bug 还是一种误解。我该如何使用 Mockito 2.x 模拟具有不同布尔参数的调用?

例子 也可以在 github 上找到。但 surefire 只会启动测试,使用

mvn test -Dtest=MockitoExample

使用Mockito 2.21执行测试会得到相同的结果。

4个回答

57

自 Mockito 2.20 版本开始,还可以在本地添加 lenient()。

@ExtendWith(MockitoExtension.class)
public class MockitoExample {

  static abstract class TestClass {
    public abstract int booleanMethod(boolean arg);
  }

  @Mock
  TestClass testClass;

  @BeforeEach
  public void beforeEach() {
    lenient().when(testClass.booleanMethod(eq(true))).thenReturn(1);
    lenient().when(testClass.booleanMethod(eq(false))).thenReturn(2);
  }

  @Test
  public void test() {
    assertEquals(1,testClass.booleanMethod(true));
    assertEquals(2,testClass.booleanMethod(false));
  }
}

1
你的回答似乎提供了更好的建议。我写了一个答案来完善你的方法(+1)。今天我花费了超过1个小时去猜测一个非常复杂的代码问题... - davidxxx
3
我同意这个应该被接受,也许需要一些解释为什么需要它。 - Paweł Sosnowski

47

在Mockito的严格模式下(默认行为),在同一个方法上调用多个when将会重置该模拟对象。解决方案是只调用一次when,并将逻辑放在一个Answer中:

@BeforeEach
public void beforeEach() {
    when(testClass.booleanMethod(anyBoolean())).thenAnswer(invocationOnMock -> {
        if ((boolean) invocationOnMock.getArguments()[0]) {
            return 1;
        }
        return 2;
    });
}

或者,您可以使用宽松的模拟,但这并不总是一个好主意 - 宽松的模拟允许冗余存根,并使您在测试中容易犯错误,这可能导致“生产”代码中未被发现的错误:

@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
public class MockitoExample {

13
真的吗?我必须使用Answers,解释那里的论点,尽管最初已经实现了非常好的DSL,并使存根更容易出错?如果是这样的话,似乎最好放弃mockito的使用,注入真实模拟(至少,如果您正在使用spring或weld)。 - aschoerk
1
抱歉,非常感谢您的回答。那似乎是前进的一种方式。 - aschoerk
org.mockito.quality.Strictness.LENIENT - amdev
2
如果你可以接受 doReturn().when() 语法,那么转换成它是一个不错的方法。 - amoe
Mock在多次使用不同参数的when时没有重置 - Ivan

28

Mockito 1和2的"严谨程度"不同。此外,使用JUnit 4或5的Mockito 2时,默认级别仍然不同。

总结:

3个严格级别:

  • LENIENT:最小的严格性
  • WARN:向控制台发出额外的警告
  • STRICT_STUBS:通过抛出异常确保干净的测试,但也可能产生一些错误的结果。

根据使用的API的默认有效级别:

  • Mockito 1:LENIENT
  • 使用JUnit 4的Mockito 2:WARN
  • 使用JUnit 5(MockitoExtension.class)的Mockito 2:STRICT_STUBS
  • Mockito 3:计划为STRICT_STUBS

更多细节

实际上,Mockito文档非常清楚:

Strictness javadoc指出:

配置Mockito在模拟会话期间的“严格程度”。会话通常映射到单个测试方法调用。严格性可以推动更干净的测试和更好的生产力。充分利用增强的Strictness的最简单方法是使用Mockito的JUnit支持(MockitoRule或MockitoJUnitRunner)。如果您无法使用JUnit支持,则应使用MockitoSession。

严格程度如何影响测试(模拟会话)的行为?

1.Strictness.LENIENT - 不添加任何行为。Mockito 1.x的默认值。仅建议在不能使用STRICT_STUBS或WARN时使用。

2.Strictness.WARN - 帮助保持测试整洁并提高可调试性。报告有关未使用的存根和存根参数不匹配的控制台警告(参见org.mockito.quality.MockitoHint)。当使用JUnitRule或MockitoJUnitRunner时,Mockito 2.x的默认行为。如果您无法使用STRICT_STUBS,则建议使用。

3.Strictness.STRICT_STUBS - 确保干净的测试,减少测试代码重复,提高调试性能。灵活性和生产力的最佳组合。强烈推荐。计划为Mockito v3的默认值。请参阅 STRICT_STUBS 了解详细信息。

但是,与消息关联的抛出异常无论如何都可能过于严格。

"具有不同参数的以下存根方法"。

例外消息证明了这种情况:

然而,有些合法的方案会导致这个例外产生错误的负面信号:

...

  • 被测试代码有意使用不同的参数调用存根方法

因此,默认情况下禁止它似乎过于严格。如果您使用JUnit 5,作为STRICT_STUBS的替代方案,您可以使用WARNING,但通常应避免使用太安静的

import java.util.List;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;

@ExtendWith(MockitoExtension.class)
public class FooTest {

    @MockitoSettings(strictness = Strictness.WARN)
    @Test
    void foo() throws Exception {
        List<String> strings = Mockito.mock(List.class);
        Mockito.when(strings.add("a"))
               .thenReturn(true);
        Mockito.when(strings.add("b"))
               .thenReturn(false);
    }

    @Test
    void fooKo() throws Exception {
        List<String> strings = Mockito.mock(List.class);
        Mockito.when(strings.add("a"))
               .thenReturn(true);
        Mockito.when(strings.add("b"))
               .thenReturn(false);

    }

}

fooKo() 抛出误用 Mockito 异常而 foo() 成功,但提供有用的警告:

[MockitoHint] FooTest(请参阅 MockitoHint 的 javadoc):
[MockitoHint] 1. 未使用 -> 在 FooTest.foo(FooTest.java:19)
[MockitoHint] 2. 未使用 -> 在 FooTest.foo(FooTest.java:21)

作为另一种选择,您还可以使用 Mockito.lenient() ,如 aschoerk 所描述的那样,为特定调用应用宽松严格性。您还可以在模拟实例化时将每个模拟调用设置为宽松。

@Test
void foo() throws Exception {
    List<String> strings = Mockito.mock(List.class, Mockito.withSettings()
                                                           .lenient());
     ....
}

-4

由于第一个答案让我感到惊讶,我进行了以下检查:

interface Poops {
    String get(boolean is);
}

@Test
void test1() {
    Poops a = mock(Poops.class);

    when(a.get(eq(true))).thenReturn("1");
    when(a.get(eq(false))).thenReturn("2");

    Assertions.assertEquals("1", a.get(true));
    Assertions.assertEquals("2", a.get(false));
}

它适用于Mockito 2.21.0。

更新: 问题似乎是Jupiter Mockito扩展,它将默认设置更改为Strictness.STRICT_STUBS


如果你有一个新问题,请通过点击提问按钮来提出。如果有需要,可以包含此问题的链接以提供上下文。- 来自评论 - Draken
@aschoerk 我克隆了你的repo,CD到mockito-example,删除了pom依赖项ejb-cdi-unit,因为我没有它,然后就可以运行mvn test -> BUILD SUCCESS。但在IntelliJ中失败了。嗯。 - johanneslink
@johanneslink 请阅读有关此主题的元信息这里。本节仅用于回答问题,而不是对话。如果您需要进一步澄清,请使用评论进行提问。您不应该只使用答案区域,因为它更符合您的需求。 - Draken
@aschoerk,区别在于扩展。如果我删除@ExtendWith(MockitoExtension.class),test1就可以工作了。也许MockitoExtensions更改了一些Mockito配置。 - johanneslink
@Draken 定义的 interface Poops { String get(boolean is); } 看起来很好。是的。@Test void test1() { Poops a = mock(Poops.class);when(a.get(eq(true))).thenReturn("1"); when(a.get(eq(false))).thenReturn("2"); Assertions.assertEquals("1", a.get(true)); Assertions.assertEquals("2", a.get(false));} - johanneslink
显示剩余8条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接