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

Simulation of Service using Mockito 2 leads to stubbing error

我尝试使用 Mockito 模拟 class 的行为。 这使用 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 中的错误还是误解。 should/can 我如何使用 Mockito 2.x 来模拟具有不同布尔参数的调用?

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

开始测试
mvn test -Dtest=MockitoExample

使用 Mockito 2.21 执行测试会导致相同的结果。

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

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

或者,您可以使用宽松的模拟,但这并不总是一个好主意 - 宽松的模拟允许冗余存根,并使您更容易在测试中犯错误,这可能会导致 "production"代码:

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

由于第一个答案出人意料,我检查了以下内容:

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

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

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

总结一下:

3 个严格级别:

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

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

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

更多详情

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

Strictness javadoc 状态:

Configures the "strictness" of Mockito during a mocking session.A session typically maps to a single test method invocation. Strictness drives cleaner tests and better productivity.The easiest way to leverage enhanced Strictness is usingMockito's JUnit support (MockitoRule or MockitoJUnitRunner).If you cannot use JUnit support MockitoSession is the way to go.

How strictness level influences the behavior of the test (mocking session)?

1.Strictness.LENIENT - no added behavior.The default of Mockito 1.x.Recommended only if you cannot use STRICT_STUBS nor WARN.

2.Strictness.WARN - helps keeping tests clean and improves debuggability.Reports console warnings about unused stubsand stubbing argument mismatch (see org.mockito.quality.MockitoHint).The default behavior of Mockito 2.x when JUnitRule or MockitoJUnitRunner are used. Recommended if you cannot use STRICT_STUBS.

3.Strictness.STRICT_STUBS - ensures clean tests, reduces test code duplication, improves debuggability.Best combination of flexibility and productivity. Highly recommended.Planned as default for Mockito v3.See STRICT_STUBS for the details.

但是无论抛出与消息关联的异常是什么

"has following stubbing(s) with different arguments"

似乎是一个过于严格的检查。 异常消息在某种程度上证明了这一点:

However, there are legit scenarios when this exception generates false negative signal:

...

  • stubbed method is intentionally invoked with different arguments by code under test

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

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

这是一个例子:

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 (see javadoc for MockitoHint):
[MockitoHint] 1. Unused -> at FooTest.foo(FooTest.java:19)
[MockitoHint] 2. Unused -> at FooTest.foo(FooTest.java:21)

作为其他选择,您还可以使用 Mockito.lenient() 很好地描述了 aschoerk 对特定调用应用宽松的严格性。 以及您可以在模拟实例化时将每个模拟调用设置为宽松:

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