kni*_*ttl 3 java junit unit-testing mocking mockito
前言:
这个问题和答案旨在作为对由于误用 Mockito 或误解 Mockito 如何工作以及与用 Java 语言编写的单元测试交互而产生的大多数问题的规范答案。
我已经实现了一个应该进行单元测试的类。请注意,此处显示的代码只是一个虚拟实现,Random
仅供说明之用。真实的代码将使用真实的依赖项,例如另一个服务或存储库。
public class MyClass {
public String doWork() {
final Random random = new Random(); // the `Random` class will be mocked in the test
return Integer.toString(random.nextInt());
}
}
Run Code Online (Sandbox Code Playgroud)
我想使用 Mockito 来模拟其他类,并编写了一个非常简单的 JUnit 测试。但是,我的类在测试中没有使用模拟:
public class MyTest {
@Test
public void test() {
Mockito.mock(Random.class);
final MyClass obj = new MyClass();
Assertions.assertEquals("0", obj.doWork()); // JUnit 5
// Assert.assertEquals("0", obj.doWork()); // JUnit 4
// this fails, because the `Random` mock is not used :(
}
}
Run Code Online (Sandbox Code Playgroud)
即使使用 (JUnit 4) 运行测试或使用(JUnit 5)MockitoJUnitRunner
扩展并使用注释也无济于事;真正的实现仍然使用:MockitoExtension
@Mock
@ExtendWith(MockitoExtension.class) // JUnit 5
// @RunWith(MockitoJUnitRunner.class) // JUnit 4
public class MyTest {
@Mock
private Random random;
@Test
public void test() {
final MyClass obj = new MyClass();
Assertions.assertEquals("0", obj.doWork()); // JUnit 5
// Assert.assertEquals("0", obj.doWork()); // JUnit 4
// `Random` mock is still not used :((
}
}
Run Code Online (Sandbox Code Playgroud)
为什么没有使用模拟类,即使在测试我的类之前调用 Mockito 方法或使用 Mockito 扩展/运行器执行测试?
该问题的其他变体包括但不限于:
thenReturn
未获荣誉 / MockitothenAnswer
未获荣誉@InjectMocks
不工作@Mock
不工作Mockito.mock
不工作kni*_*ttl 11
TLDR:存在两个或多个不同的模拟实例。您的测试正在使用一个实例,而被测试的类正在使用另一个实例。或者您在类中根本没有使用模拟,因为您new
在类中创建了对象。
模拟是实例(这就是为什么它们也被称为“模拟对象”)。调用Mockito.mock
一个类将返回该类的模拟对象。它必须分配给一个变量,然后可以将其传递给相关方法或作为依赖项注入其他类。它不会修改类本身!想想看:如果这是真的,那么该类的所有实例都会以某种方式神奇地转换为模拟。这将使得无法模拟使用多个实例的类或 JDK 中的类,例如List
或Map
(一开始就不应该模拟,但这是一个不同的故事)。
@Mock
对于使用 Mockito 扩展/运行器进行注释也是如此:创建一个模拟对象的新实例@Mock
,然后将其分配给用 注释的字段(或参数) 。该模拟对象仍然需要传递给正确的方法或作为依赖项注入。
避免这种混乱的另一种方法是:new
在 Java 中,将始终为对象分配内存,并初始化真实类的新实例。覆盖 的行为是不可能的new
。即使是像 Mockito 这样聪明的框架也无法做到这一点。
\xc2\xbb但是我怎样才能模拟我的类呢?\xc2\xab 你会问。更改类的设计以使其可测试!每次您决定使用 时new
,您就将自己委托给这个确切类型的实例。根据您的具体用例和要求,存在多种选项,包括但不限于:
下面您将找到每个选项的示例实现(带或不带 Mockito 运行器/扩展):
\npublic class MyClass {\n public String doWork(final Random random) {\n return Integer.toString(random.nextInt());\n }\n}\n\npublic class MyTest {\n @Test\n public void test() {\n final Random mockedRandom = Mockito.mock(Random.class);\n final MyClass obj = new MyClass();\n Assertions.assertEquals("0", obj.doWork(mockedRandom)); // JUnit 5\n // Assert.assertEquals("0", obj.doWork(mockedRandom)); // JUnit 4\n }\n}\n\n@ExtendWith(MockitoExtension.class) // JUnit 5\n// @RunWith(MockitoJUnitRunner.class) // JUnit 4\npublic class MyTestAnnotated {\n @Mock\n private Random random;\n\n @Test\n public void test() {\n final MyClass obj = new MyClass();\n Assertions.assertEquals("0", obj.doWork(random)); // JUnit 5\n // Assert.assertEquals("0", obj.doWork(random)); // JUnit 4\n }\n}\n
Run Code Online (Sandbox Code Playgroud)\npublic class MyClass {\n private final Random random;\n\n public MyClass(final Random random) {\n this.random = random;\n }\n\n // optional: make it easy to create "production" instances (although I wouldn\'t recommend this)\n public MyClass() {\n this(new Random());\n }\n\n public String doWork() {\n return Integer.toString(random.nextInt());\n }\n}\n\npublic class MyTest {\n @Test\n public void test() {\n final Random mockedRandom = Mockito.mock(Random.class);\n final MyClass obj = new MyClass(mockedRandom);\n // or just obj = new MyClass(Mockito.mock(Random.class));\n Assertions.assertEquals("0", obj.doWork()); // JUnit 5\n // Assert.assertEquals("0", obj.doWork()); // JUnit 4\n }\n}\n\n@ExtendWith(MockitoExtension.class) // JUnit 5\n// @RunWith(MockitoJUnitRunner.class) // JUnit 4\npublic class MyTestAnnotated {\n @Mock\n private Random random;\n\n @Test\n public void test() {\n final MyClass obj = new MyClass(random);\n Assertions.assertEquals("0", obj.doWork()); // JUnit 5\n // Assert.assertEquals("0", obj.doWork()); // JUnit 4\n }\n}\n
Run Code Online (Sandbox Code Playgroud)\n根据依赖项的构造函数参数的数量和表达代码的需要,可以使用 JDK 中的现有接口(Supplier
、Function
、BiFunction
)或引入自定义工厂接口(@FunctionInterface
如果它只有一个方法,则用 进行注释)。
以下代码将选择自定义界面,但可以与Supplier<Random>
.
@FunctionalInterface\npublic interface RandomFactory {\n Random newRandom();\n}\n\npublic class MyClass {\n private final RandomFactory randomFactory;\n\n public MyClass(final RandomFactory randomFactory) {\n this.randomFactory = randomFactory;\n }\n\n // optional: make it easy to create "production" instances (again: I wouldn\'t recommend this)\n public MyClass() {\n this(Random::new);\n }\n\n public String doWork() {\n return Integer.toString(randomFactory.newRandom().nextInt());\n }\n}\n\npublic class MyTest {\n @Test\n public void test() {\n final RandomFactory randomFactory = () -> Mockito.mock(Random.class);\n final MyClass obj = new MyClass(randomFactory);\n Assertions.assertEquals("0", obj.doWork()); // JUnit 5\n // Assert.assertEquals("0", obj.doWork()); // JUnit 4\n }\n}\n\n@ExtendWith(MockitoExtension.class) // JUnit 5\n// @RunWith(MockitoJUnitRunner.class) // JUnit 4\npublic class MyTestAnnotated {\n @Mock\n private RandomFactory randomFactory;\n\n @Test\n public void test() {\n // this is really awkward; it is usually simpler to use a lambda and create the mock manually\n Mockito.when(randomFactory.newRandom()).thenAnswer(a -> Mockito.mock(Random.class));\n final MyClass obj = new MyClass(randomFactory);\n Assertions.assertEquals("0", obj.doWork()); // JUnit 5\n // Assert.assertEquals("0", obj.doWork()); // JUnit 4\n }\n}\n
Run Code Online (Sandbox Code Playgroud)\n@InjectMocks
\n\n但我正在使用
\n@InjectMocks
调试器并验证我在测试类中是否有模拟。然而,我设置的模拟方法Mockito.mock
从未Mockito.when
被调用!(换句话说:“我得到一个 NPE”、“我的集合为空”、“返回默认值”等)\xe2\x80\x94 一个困惑的开发人员,大约。2022年
\n
用代码来表达,上面的引用看起来像这样:
\n@ExtendWith(MockitoExtension.class) // JUnit 5\n// @RunWith(MockitoJUnitRunner.class) // JUnit 4\npublic class MyTestAnnotated {\n @Mock\n private Random random;\n\n @InjectMocks\n private MyClass obj;\n\n @Test\n public void test() {\n random = Mockito.mock(Random.class);\n Mockito.when(random.nextInt()).thenReturn(42);\n Assertions.assertEquals("42", obj.doWork()); // JUnit 5\n // Assert.assertEquals("42", obj.doWork()); // JUnit 4\n }\n}\n
Run Code Online (Sandbox Code Playgroud)\n上面代码的问题在于方法中的第一行:它创建一个新的test()
模拟实例并将其分配给该字段,从而有效地覆盖现有值。而是将原始值注入到被测试的类中 ( )。创建的实例仅存在于测试中,而不存在于被测试的类中。@InjectMocks
obj
Mockito.mock
这里的操作顺序是:
\n@Mock
带注释的字段都会分配一个新的模拟对象。@InjectMocks
获取对模拟对象的注入引用。Mockito.mock
)的不同引用覆盖。原始参考已丢失,并且在测试类中不再可用。obj
) 仍然保留对初始模拟实例的引用并使用它。该测试仅引用新的模拟实例。这基本上可以归结为Java 是“按引用传递”还是“按值传递”?。
\n您可以使用调试器验证这一点。设置断点,然后比较测试类和被测类中模拟字段的对象地址/ID。您会注意到这是两个不同的、不相关的对象实例。
\n解决方案?不要覆盖引用,而是设置通过注释创建的模拟实例。只需摆脱重新分配即可Mockito.mock
:
@ExtendWith(MockitoExtension.class) // JUnit 5\n// @RunWith(MockitoJUnitRunner.class) // JUnit 4\npublic class MyTestAnnotated {\n @Mock\n private Random random;\n\n @InjectMocks\n private MyClass obj;\n\n @Test\n public void test() {\n // this.random must not be re-assigned!\n Mockito.when(random.nextInt()).thenReturn(42);\n Assertions.assertEquals("42", obj.doWork()); // JUnit 5\n // Assert.assertEquals("42", obj.doWork()); // JUnit 4\n }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n\n我听从了您的建议并使用依赖项注入手动将模拟传递到我的服务中。它仍然不起作用,我的测试因空指针异常而失败,有时甚至在单个测试方法实际运行之前。你撒谎了,兄弟!
\n\xe2\x80\x94 另一个困惑的开发人员,2022 年末
\n
该代码可能类似于:
\n@ExtendWith(MockitoExtension.class) // JUnit 5\n// @RunWith(MockitoJUnitRunner.class) // JUnit 4\npublic class MyTestAnnotated {\n @Mock\n private Random random;\n\n private final MyClass obj = new MyClass(random);\n\n @Test\n public void test() {\n Mockito.when(random.nextInt()).thenReturn(42);\n Assertions.assertEquals("42", obj.doWork()); // JUnit 5\n // Assert.assertEquals("42", obj.doWork()); // JUnit 4\n }\n}\n
Run Code Online (Sandbox Code Playgroud)\n这与第一个推论非常相似,可以归结为对象生命周期和引用与值。上面代码中发生的步骤如下:
\nMyTestAnnotated
测试框架创建了一个新实例(例如new MyTestAnnotated()
)。private MyClass obj = new MyClass(random);
。此时,该random
字段仍具有其默认值null
\xe2\x86\x92 the obj
field is located new MyClass(null)
。@Mock
带注释的字段都会分配一个新的模拟对象。这不会更新中的值MyService obj
,因为它null
最初是通过的,而不是对模拟的引用。根据您的MyService
实现,在创建测试类的实例时这可能已经失败(MyService
可能在构造函数中对其依赖项执行参数验证);或者它可能仅在执行测试方法时失败(因为依赖项为空)。
解决方案?熟悉对象生命周期、字段初始值设定项顺序以及模拟框架可以/将注入其模拟并更新引用(以及更新哪些引用)的时间点。尽量避免将“神奇”框架注释与手动设置混合在一起。手动创建所有内容(模拟、服务),或者将初始化移至用@Before
(JUnit 4) 或@BeforeEach
(JUnit 5) 注释的方法。
@ExtendWith(MockitoExtension.class) // JUnit 5\n// @RunWith(MockitoJUnitRunner.class) // JUnit 4\npublic class MyTestAnnotated {\n @Mock\n private Random random;\n\n private MyClass obj;\n\n @BeforeEach // JUnit 5\n // @Before // JUnit 4\n public void setup() {\n obj = new MyClass(random);\n }\n\n @Test\n public void test() {\n Mockito.when(random.nextInt()).thenReturn(42);\n Assertions.assertEquals("42", obj.doWork()); // JUnit 5\n // Assert.assertEquals("42", obj.doWork()); // JUnit 4\n }\n}\n
Run Code Online (Sandbox Code Playgroud)\n或者,手动设置所有内容,无需注释,这需要自定义运行程序/扩展:
\npublic class MyTest {\n private Random random;\n private MyClass obj;\n\n @BeforeEach // JUnit 5\n // @Before // JUnit 4\n public void setup() {\n random = Mockito.mock(random);\n obj = new MyClass(random);\n }\n\n @Test\n public void test() {\n Mockito.when(random.nextInt()).thenReturn(42);\n Assertions.assertEquals("42", obj.doWork()); // JUnit 5\n // Assert.assertEquals("42", obj.doWork()); // JUnit 4\n }\n}\n
Run Code Online (Sandbox Code Playgroud)\n 归档时间: |
|
查看次数: |
12115 次 |
最近记录: |