前言:
这个问题和答案旨在作为由于误用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());
}
}
我想使用Mockito来模拟其他类,并编写了一个非常简单的JUnit测试。然而,我的类在测试中没有使用mock:
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 :(
}
}
即使使用MockitoJUnitRunner
(JUnit 4)运行测试或使用MockitoExtension
(JUnit 5)扩展并使用@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 :((
}
}
为什么不使用模拟类,即使在测试类之前调用Mockito方法,或者使用Mockito扩展/运行器执行测试?
此问题的其他变体包括但不限于:
- 我的模拟返回null /我的存根返回null
- 使用Mockito时出现NullPointerException
- 我的模拟在测试中为空
- My mocks do not return the expected value / My stubs do not return the expected value
- Mockito
thenReturn
未得到认可/ MockitothenAnswer
未得到认可 @InjectMocks
不工作@Mock
不工作Mockito.mock
不工作- My class is not using mocks /我的类没有使用存根
- 我的测试仍然调用或执行模拟/存根类的真实的实现
1条答案
按热度按时间9gm1akwq1#
TLDR:您的mock存在两个或多个不同的示例。您的测试使用一个示例,而您的被测类使用另一个示例。或者您在类中根本没有使用mock,因为您
new
了类中的对象。问题概述(类vs示例)
Mock是 instances(这就是为什么它们也被称为“mock object”)。在类上调用
Mockito.mock
将返回该类的mock object。它必须被分配给一个变量,然后可以传递给相关方法或作为依赖项注入到其他类中。它不会 * 修改类本身!想想看:如果这是真的,那么类的所有示例都将以某种方式神奇地转换为mock。这将使得无法模拟使用多个示例的类或来自JDK的类,如List
或Map
(首先不应该被模拟,但这是另一回事)。对于使用Mockito扩展/runner的
@Mock
注解也是如此:创建一个mock对象的新 instance,然后将其分配给用@Mock
注解的字段(或参数)。这个mock对象仍然需要传递给正确的方法或作为依赖项注入。另一种避免这种混淆的方法是:Java中的
new
将 * 始终 * 为对象分配内存,并将初始化真实的类的这个新示例。不可能覆盖new
的行为。即使是像Mockito这样聪明的框架也做不到。解决方案
»但是我怎么能模拟我的类呢?«你会问。改变你的类的设计,使其可测试!每次你决定使用
new
时,你都要将自己提交给一个完全相同类型的示例。根据你的具体用例和需求,有多种选择,包括但不限于:1.如果你可以改变方法的签名/接口,传递(mock)示例作为方法参数。这要求示例在所有调用站点都可用,这可能并不总是可行的。
1.如果你不能改变方法的签名,inject the dependency在你的构造函数中,并将它存储在一个字段中,以便以后由方法使用。
1.有时,示例必须在方法被调用时创建,而不是在方法被调用之前创建。在这种情况下,您可以引入另一个间接层并使用称为abstract factory pattern的东西。工厂对象将创建并返回依赖项的示例。工厂可以存在多个实现:一个返回真实的的依赖关系,另一个返回测试双精度型,例如mock。
下面您将找到每个选项的示例实现(有和没有Mockito runner/extension):
更改方法签名
构造函数依赖注入
工厂延迟施工
根据依赖项的构造函数参数的数量和对表达性代码的需求,可以使用JDK中的现有接口(
Supplier
,Function
,BiFunction
)或引入自定义工厂接口(如果只有一个方法,则用@FunctionInterface
注解)。下面的代码将选择一个自定义接口,但在
Supplier<Random>
上也能正常工作。推论:(错误)使用
@InjectMocks
但是我使用的是
@InjectMocks
,并使用调试器验证了我在测试中的类中有mock。然而,我用Mockito.mock
和Mockito.when
设置的mock方法从未被调用!(换句话说:“我得到一个NPE”、“我的集合是空的”、“返回默认值”等)如果用代码表示,上面的引用看起来像这样:
上面代码的问题是
test()
方法的第一行:它创建并分配一个 new mock示例给字段,有效地覆盖了现有的值。但是@InjectMocks
将原始值注入到测试中的类(obj
)中。用Mockito.mock
创建的示例只存在于测试中,而不存在于测试中的类中。这里的操作顺序是:
1.所有带
@Mock
注解的字段都被分配了一个新的模拟对象。@InjectMocks
注解字段被注入 references 到步骤1中的模拟对象。1.测试类中的引用被新的mock对象(通过
Mockito.mock
创建)的不同引用覆盖。原始引用丢失,在测试类中不再可用。1.被测类(
obj
)仍然持有对初始模拟示例的引用并使用它。测试只有对新模拟示例的引用。这基本上归结为Is Java "pass-by-reference" or "pass-by-value"?。
你可以用调试器来验证这一点。设置一个断点,然后比较测试类和被测类中模拟字段的对象地址/id。你会注意到这是两个不同的、不相关的对象示例。
解决方案?不要覆盖引用,而是设置通过注解创建的mock示例。只需使用
Mockito.mock
消除重新赋值即可:推论:对象生命周期和神奇的框架注解
我听从了你的建议,使用依赖注入手动将mock传递到我的服务中。它仍然不起作用,我的测试失败,出现空指针异常,有时甚至在单个测试方法实际运行之前。你撒谎了,兄弟!
代码可能看起来像这样:
这与第一个推论非常相似,归结为对象生命周期和引用与值。上面代码中发生的步骤如下:
MyTestAnnotated
的新示例由测试框架创建(例如new MyTestAnnotated()
)。1.所有的构造函数和字段初始化器都会被执行。这里没有构造函数,只有一个字段初始化器:此时,
random
字段仍具有其默认值null
-obj
字段被分配new MyClass(null)
。1.所有带
@Mock
注解的字段都被分配了一个新的mock对象。这不会更新MyService obj
中的值,因为它最初传递给null
,而不是对mock的引用。根据您的
MyService
实现,在创建测试类的示例时,这可能已经失败了(MyService
可能会在构造函数中执行其依赖项的参数验证);或者它可能只在执行测试方法时失败(因为依赖项为空)。解决方案?熟悉对象生命周期、字段初始化器顺序以及mock框架可以/将注入其mock和更新引用的时间点(以及更新哪些引用)。尽量避免将“神奇”框架注解与手动设置混合在一起。要么手动创建所有内容(mocks,service),或者将初始化移动到用
@Before
(JUnit 4)或@BeforeEach
(JUnit 5)注解的方法。或者,手动设置所有内容,而不需要注解,这需要自定义runner/extension:
参考资料