当前位置:Java -> 解密用Mockito进行静态模拟
如今,编写测试是开发的标准组成部分。不幸的是,我们不时需要处理这样一种情况,即测试组件调用了一个静态方法。我们的目标是减轻这一部分,避免第三方组件的行为。本文将介绍使用Mockito 3.4版本引入的“行内模拟生成器”来模拟静态方法。换句话说,本文将解释Mockito.mockStatic方法,以帮助我们避免不需要的静态方法调用。
很多时候,我们要处理这样一种情况,即我们的代码调用了一个静态方法。它可以是我们自己的代码(例如,一些实用类或第三方库的类)。单元测试的主要关注点是专注于被测试的组件,而忽略任何其他组件的行为(包括静态方法)。一个例子是,当A组件中的被测试方法调用了与组件B无关的静态方法。
尽管如此,不建议使用静态方法;我们却经常看到它们(例如实用类)。避免使用静态方法的原因在这篇文章用Mockito模拟静态方法中总结得非常好。
一般来说,有些人可能会说,在编写清晰的面向对象代码时,我们不应该需要模拟静态类。 这通常意味着设计问题或是我们应用程序中的代码异味。
为什么呢?首先,依赖静态方法的类具有紧密耦合,其次,它几乎总是导致难以测试的代码。理想情况下,一个类不应该负责获取它的依赖项,如果可能的话,它们应该被外部注入。
因此,是否可以重构我们的代码,使其更具测试性,值得考虑。当然,这并非总是可能的,有时我们需要模拟静态方法。
让我们定义一个简单的SequenceGenerator
实用类,用作本文中测试的目标。这个类有两个“愚蠢”的静态方法(它们没有什么特别之处)。第一个nextId
方法(第10-12行)每次调用时生成一个新的ID,第二个nextMultipleIds
方法(第14-20行)按传入的参数生成多个ID。
@UtilityClass
public class SequenceGenerator {
private static AtomicInteger counter;
static {
counter = new AtomicInteger(1);
}
public static int nextId() {
return counter.getAndIncrement();
}
public static List<Integer> nextMultipleIds(int count) {
var newValues = new ArrayList<Integer>(count);
for (int i = 0; i < count; i++) {
newValues.add(counter.getAndIncrement());
}
return newValues;
}
}
MockedStatic
对象为了能够模拟静态方法,我们需要通过“行内模拟生成器”将受影响的类包装起来。上述SequenceGenerator
类的静态方法可以通过Mockito.mockStatic
方法获得MockedStatic
实例进行模拟。这可以这样做:
try (MockedStatic<SequenceGenerator> seqGeneratorMock = mockStatic(SequenceGenerator.class)) {
...
}
或
MockedStatic<SequenceGenerator> seqGeneratorMock = mockStatic(SequenceGenerator.class));
...
seqGeneratorMock.close();
创建的mockStatic
实例必须始终关闭。否则,当涉及相同的静态方法(即我们的情况SequenceGenerator
)时,在同一线程中运行的下一个测试可能会出现丑陋的副作用。因此,第一种选择似乎更好,并且在这个主题的大多数文章中都在使用。这个解释可以在JavaDoc网站(第48章)上找到:
使用行内模拟生成器时,可以在当前线程和用户定义的范围内模拟静态方法调用。这样,Mockito确保同时运行的测试不会相互干扰。为了确保静态模拟保持临时状态,建议在try-with-resources结构中定义范围。
要了解更多关于这个主题的知识,请查看这些有用的链接:
静态方法(例如我们上面定义的nextId
或nextMultipleIds
方法)可以通过MockedStatic.when
进行模拟。此方法接受MockedStatic.Verification
定义的功能接口。我们可以处理两种情况。
最简单的情况是模拟没有参数的静态方法(我们的情况下是nextId
方法)。在这种情况下,只需将方法引用(见第5行)传递给seqGeneratorMock.when
方法即可。返回值按照标准方式指定(例如使用thenReturn
方法)。
@Test
void whenWithoutArgument() {
try (MockedStatic<SequenceGenerator> seqGeneratorMock = mockStatic(SequenceGenerator.class)) {
int newValue = 5;
seqGeneratorMock.when(SequenceGenerator::nextId).thenReturn(newValue);
assertThat(SequenceGenerator.nextId()).isEqualTo(newValue);
}
}
通常,我们有一个带有一些参数的静态方法 (我们的情况下是nextMultipleIds
)。然后,我们需要使用lambda表达式而不是方法引用(见第5行)。同样,我们可以使用标准方法(例如then
,thenRetun
,thenThrow
等)来处理带有所需行为的响应。
@Test
void whenWithArgument() {
try (MockedStatic<SequenceGenerator> seqGeneratorMock = mockStatic(SequenceGenerator.class)) {
int newValuesCount = 5;
seqGeneratorMock.when(() -> SequenceGenerator.nextMultipleIds(newValuesCount))
.thenReturn(List.of(1, 2, 3, 4, 5));
assertThat(SequenceGenerator.nextMultipleIds(newValuesCount)).hasSize(newValuesCount);
}
}
类似地,我们还可以通过调用seqGeneratorMock.verify
方法对被模拟组件的调用进行验证(见第7行)
@Test
void verifyUsageWithoutArgument() {
try (MockedStatic<SequenceGenerator> seqGeneratorMock = mockStatic(SequenceGenerator.class)) {
var person = new Person("Pamela");
seqGeneratorMock.verify(SequenceGenerator::nextId);
assertThat(person.getId()).isEqualTo(0);
}
}
或者使用lambda表达式(见第6行)。
@Test
void verifyUsageWithArgument() {
try (MockedStatic<SequenceGenerator> seqGeneratorMock = mockStatic(SequenceGenerator.class)) {
List<Integer> nextIds = SequenceGenerator.nextMultipleIds(3);
seqGeneratorMock.verify(() -> SequenceGenerator.nextMultipleIds(ArgumentMatchers.anyInt()));
assertThat(nextIds).isEmpty();
}
}
注意:请注意,seqGeneratorMock
在这里不提供任何价值,因为静态方法仍然使用默认值进行模拟。目前还没有间谍版本。因此,任何预期的返回值都必须进行模拟,或者将返回默认值。
在Mockito 5.x中,默认情况下启用了mockStatic
功能。因此,无需进行特殊设置。但是,对于旧版本(例如4.x),我们需要设置Mockito。
如前所述,在5.x版本中无需进行任何设置。请参阅GitHub存储库中的说明:
Mockito 5 切换了默认的模拟程序至 mockito-inline,并且现在需要 Java 11。
当使用较旧的版本并通过mock-inline
功能使用mockStatic
时,可能会出现以下错误:
org.mockito.exceptions.base.MockitoException:
The used MockMaker SubclassByteBuddyMockMaker does not support the creation of static mocks
Mockito's inline mock maker supports static mocks based on the Instrumentation API.
You can simply enable this mock mode, by placing the 'mockito-inline' artifact where you are currently using 'mockito-core'.
Note that Mockito's inline mock maker is not supported on Android.
at com.github.aha.poc.junit.person.StaticUsageTests.mockStaticNoArgValue(StaticUsageTests.java:15)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
通常,有两种选项可以用于启用此功能的Mockito版本(请参阅所有Mockito版本此处)。
第一种选项是向我们的Maven项目中添加<project>\src\test\resources\mockito-extensions\org.mockito.plugins.MockMaker
,内容如下:
mock-maker-inline
mock-inline
依赖另一种可能更好的选择是添加mockito-inline
依赖:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>5.2.0</version>
<scope>test</scope>
</dependency>
注意:此依赖已包含上述的MockMaker
资源。因此,此选项似乎更为方便。
无论使用何种版本(见上文),Maven构建都可能会产生以下警告:
WARNING: A Java agent has been loaded dynamically (<user_profile>\.m2\repository\net\bytebuddy\byte-buddy-agent\1.12.9\byte-buddy-agent-1.12.9.jar)
WARNING: If a serviceability tool is in use, please run with -XX:+EnableDynamicAgentLoading to hide this warning
WARNING: If a serviceability tool is not in use, please run with -Djdk.instrument.traceUsage for more information
WARNING: Dynamic loading of agents will be disallowed by default in a future release
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
即使出现这些警告,Mockito仍然能够正常工作。这可能是由于/取决于使用的工具、JDK版本等造成的。
本文介绍了使用Mockito inline模拟静态方法的过程。文章从静态模拟的基础知识开始,然后演示了when
和verify
的用法(可以使用方法引用或lambda表达式)。最后,展示了不同Mockito版本的Mockito inline模拟程序的设置。
使用的源代码可以在此处找到。
推荐阅读: 35. 重载的方法能否根据返回类型进行区分?为什么?
本文链接: 解密用Mockito进行静态模拟