【问题标题】:Simulation of Service using Mockito 2 leads to stubbing error使用 Mockito 2 模拟服务导致存根错误
【发布时间】:2019-02-07 21:54:25
【问题描述】:

我尝试使用 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));
  }
}

期望的是,模拟的 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 中的错误还是误解。我应该/如何使用 Mockito 2.x 来模拟​​具有不同布尔参数的调用?

example 也可以在 github 上找到。但是,surefire 将仅使用

开始测试
mvn test -Dtest=MockitoExample

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

【问题讨论】:

    标签: java unit-testing mocking mockito junit5


    【解决方案1】:

    使用严格的存根(Mockito 的默认行为)在同一方法上调用多个 whens 将重置该模拟。解决方案是调用when 一次 并在Answer 中包含逻辑:

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

    或者,您可以使用 lenient mocking,但这并不总是一个好主意 - lenient mocking 允许冗余存根,并且使您更容易在测试中犯错误,这可能会导致“生产”代码中未被注意到的错误:

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

    【讨论】:

    • 真的吗?我必须使用 Answers,解释那里最初实现了非常精细的 DSL 的论点并使存根更容易出错?在这种情况下,最好放弃 mockito 的使用并注入真实的模拟(至少,如果您使用的是弹簧或焊接)
    • 对不起,非常感谢您的回答。这似乎是一种前进的方式。
    • org.mockito.quality.Strictness.LENIENT
    • 如果您可以使用doReturn().when() 语法,那么移植到它是一种不错的方法。
    【解决方案2】:

    从 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 个小时来猜测一个非常复杂的代码的问题......
    • 我也同意这应该被接受,也许有一些解释为什么它需要
    【解决方案3】:

    Mockito 1 和 2 的“严格”级别不同。
    除了将 Mockito 2 与 JUnit 4 或 5 一起使用之外,默认级别仍然会有所不同。

    总结一下:

    3 级严格性:

    • LENIENT:最低严格度
    • WARN :向控制台发出额外警告
    • STRICT_STUBS :如果可能被误用,则通过抛出异常来确保干净的测试,但也可能产生一些误报。

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

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

    更多详情

    实际的 Mockito 文档对此非常清楚:

    Strictness javadoc 声明:

    在模拟会话期间配置 Mockito 的“严格性”。A session 通常映射到单个测试方法调用。严格 推动更清洁的测试和更好的生产力。最简单的方法 利用增强的 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。

    但是无论抛出与消息相关的异常

    “具有以下具有不同参数的存根”

    似乎是一项过于严格的检查。 异常消息以某种方式证明了这一点:

    但是,当此异常生成 false 时,存在合法的场景 负面信号:

    ...

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

    所以默认禁止似乎太过分了。
    因此,如果您使用 JUnit 5 作为STRICT_STUBS 的替代品,您可以使用WARNING,但您通常希望避免使用太安静的LENIENT

    除了MockitoExtensionmockito-junit-jupiter 库还提供 @MockitoSettings 可以在方法级别和类级别使用。

    这是一个例子:

    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() 抛出 misuse Mockito 异常,而 foo() 成功但提供有用的警告:

    [MockitoHint] FooTest(请参阅 javadoc 了解 MockitoHint): [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

      【讨论】:

      • 如果您有新问题,请点击 按钮提出问题。如果有助于提供上下文,请包含指向此问题的链接。 - From Review
      • @Draken 作为评论会更好,但 cmets 不允许代码 sn-ps。不,我没有其他问题,因为一切都按我的预期工作。
      • @aschoerk 我克隆了你的 repo,CD 到 mockito-example,删除了对 ejb-cdi-unit 的 pom 依赖,因为我没有它,瞧:mvn test -> BUILD SUCCESS。但它在 IntelliJ 中失败了。嗯。
      • @johanneslink 我想你误解了 StackOverflow 的工作原理,它不是一个论坛。该区域仅用于回答OP的问题,而不是进一步提问。您在使用 why? 这个词时提出了一个问题,如果它是为了回答这个问题,请更新它,使其采用这种形式。答案通常不应以最后一行中的问题结束。如果您试图让 OP 调试问题并通过自我发现来找出答案,那么您需要更清楚地了解它。
      • @johanneslink 也许您没有使用“mvn clean install -Dtest=MockitoExample”,或者您使用的是旧版本的 Lenient。
      猜你喜欢
      • 1970-01-01
      • 2019-05-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多