单元测试Mockito框架

2023-05-03,,

单元测试Mockito框架

Mock 测试就是在测试过程中,对于某些 不容易构造(如 HttpServletRequest 必须在 Servlet 容器中才能构造出来)或者不容易获取 比较复杂 的对象(如 JDBC 中的 ResultSet 对象),用一个 虚拟 的对象(Mock 对象)来创建,以便测试方法。

Mockito的使用示例:

检验调对象相关行为是否被调用

import static org.mockito.Mockito.*;

// Mock creation
List mockedList = mock(List.class); // Use mock object - it does not throw any "unexpected interaction" exception
mockedList.add("one"); //调用了add("one")行为
mockedList.clear(); //调用了clear()行为 // Selective, explicit, highly readable verification
verify(mockedList).add("one"); // 检验add("one")是否已被调用
verify(mockedList).clear(); // 检验clear()是否已被调用

这里 mock 了一个 List(这里只是为了用作 Demo 示例,通常对于 List 这种简单的类对象创建而言,直接 new 一个真实的对象即可,无需进行 mock),verify() 会检验对象是否在前面已经执行了相关行为,这里 mockedListverify 之前已经执行了 add("one")clear() 行为,所以verify() 会通过。

配置/方法行为

// you can mock concrete classes, not only interfaces
LinkedList mockedList = mock(LinkedList.class);
// stubbing appears before the actual execution
when(mockedList.get(0)).thenReturn("first");
// the following prints "first"
System.out.println(mockedList.get(0));
// the following prints "null" because get(999) was not stubbed
System.out.println(mockedList.get(999));

这里对几个比较重要的点进行解析:

when(mockedList.get(0)).thenReturn("first")

这句话 Mockito 会解析为:当对象 mockedList 调用 get()方法,并且参数为 0 时,返回结果为"first",这相当于定制了我们 mock 对象的行为结果(mock LinkedList 对象为 mockedList,指定其行为 get(0),则返回结果为 "first")。

mockedList.get(999)

由于 mockedList 没有指定 get(999) 的行为,所以其结果为 null。因为 Mockito 的底层原理是使用 cglib 动态生成一个 代理类对象,因此,mock 出来的对象其实质就是一个 代理,该代理在 没有配置/指定行为 的情况下,默认返回 空值

上面的 Demo 使用的是 静态方法 mock() 模拟出一个实例,我们还可以通过注解 @Mock 也模拟出一个实例:

@Mock
private Intent mIntent; @Rule
public MockitoRule mockitoRule = MockitoJUnit.rule(); @Test
public void mockAndroid(){
Intent intent = mockIntent();
assertThat(intent.getAction()).isEqualTo("com.yn.test.mockito");
assertThat(intent.getStringExtra("Name")).isEqualTo("Whyn");
} private Intent mockIntent(){
when(mIntent.getAction()).thenReturn("com.yn.test.mockito");
when(mIntent.getStringExtra("Name")).thenReturn("Whyn");
return mIntent;
}

对于标记有 @Mock, @Spy, @InjectMocks 等注解的成员变量的 初始化 到目前为止有 2 种方法:

    JUnit 测试类添加 @RunWith(MockitoJUnitRunner.class)
    在标示有 @Before 方法内调用初始化方法:MockitoAnnotations.initMocks(Object)

上面的测试用例,对于 @Mock 等注解的成员变量的初始化又多了一种方式 MockitoRule。规则 MockitoRule 会自动帮我们调用 MockitoAnnotations.initMocks(this)实例化注解 的成员变量,我们就无需手动进行初始化了。

Mockito的重要方法:

实例化虚拟对象

// You can mock concrete classes, not just interfaces
LinkedList mockedList = mock(LinkedList.class); // Stubbing
when(mockedList.get(0)).thenReturn("first");
when(mockedList.get(1)).thenThrow(new RuntimeException()); // Following prints "first"
System.out.println(mockedList.get(0));
// Following throws runtime exception
System.out.println(mockedList.get(1));
// Following prints "null" because get(999) was not stubbed
System.out.println(mockedList.get(999)); // Although it is possible to verify a stubbed invocation, usually it's just redundant
// If your code cares what get(0) returns, then something else breaks (often even before verify() gets executed).
// If your code doesn't care what get(0) returns, then it should not be stubbed. Not convinced? See here.
verify(mockedList).get(0);

对于所有方法,mock 对象默认返回 null原始类型/原始类型包装类 默认值,或者 空集合。比如对于 int/Integer 类型,则返回 0,对于 boolean/Boolean 则返回 false
行为配置(stub)是可以被复写的:比如通常的对象行为是具有一定的配置,但是测试方法可以复写这个行为。请谨记行为复写可能表明潜在的行为太多了。
一旦配置了行为,方法总是会返回 配置值,无论该方法被调用了多少次。
最后一次行为配置是更加重要的,当你为一个带有相同参数的相同方法配置了很多次,最后一次起作用。

参数匹配

Mockito 通过参数对象的 equals() 方法来验证参数是否一致,当需要更多的灵活性时,可以使用参数匹配器:

// Stubbing using built-in anyInt() argument matcher
when(mockedList.get(anyInt())).thenReturn("element");
// Stubbing using custom matcher (let's say isValid() returns your own matcher implementation):
when(mockedList.contains(argThat(isValid()))).thenReturn("element");
// Following prints "element"
System.out.println(mockedList.get(999));
// You can also verify using an argument matcher
verify(mockedList).get(anyInt());
// Argument matchers can also be written as Java 8 Lambdas
verify(mockedList).add(argThat(someString -> someString.length() > 5));

参数匹配器 允许更加灵活的 验证行为配置。更多 内置匹配器自定义参数匹配器 例子请参考:ArgumentMatchersMockitoHamcrest

注意:如果使用了参数匹配器,那么所有的参数都需要提供一个参数匹配器。

verify(mock).someMethod(anyInt(), anyString(), eq("third argument"));
// Above is correct - eq() is also an argument matcher
verify(mock).someMethod(anyInt(), anyString(), "third argument");
// Above is incorrect - exception will be thrown because third argument is given without an argument matcher.

类似 anyObject()eq() 这类匹配器并不返回匹配数值。他们内部记录一个 匹配器堆栈 并返回一个空值(通常为 null)。这个实现是为了匹配 java 编译器的 静态类型安全,这样做的后果就是你不能在 检验/配置方法 外使用 anyObject()eq() 等方法。

校验次数

LinkedList mockedList = mock(LinkedList.class);
// Use mock
mockedList.add("once");
mockedList.add("twice");
mockedList.add("twice");
mockedList.add("three times");
mockedList.add("three times");
mockedList.add("three times"); // Follow two verifications work exactly the same - times(1) is used by default
verify(mockedList).add("once");
verify(mockedList, times(1)).add("once"); // Exact number of invocations verification
verify(mockedList, times(2)).add("twice");
verify(mockedList, times(3)).add("three times"); // Verification using never(). never() is an alias to times(0)
verify(mockedList, never()).add("never happened"); // Verification using atLeast()/atMost()
verify(mockedList, atLeastOnce()).add("three times");
verify(mockedList, atLeast(2)).add("three times");
verify(mockedList, atMost(5)).add("three times");

校验次数方法常用的有如下几个:

Method Meaning
times(n) 次数为n,默认为1(times(1))
never() 次数为0,相当于times(0)
atLeast(n) 最少n次
atLeastOnce() 最少一次
atMost(n) 最多n次

抛出异常

doThrow(new RuntimeException()).when(mockedList).clear();
// following throws RuntimeException
mockedList.clear();

按顺序校验

有时对于一些行为,有先后顺序之分,所以,当我们在校验时,就需要考虑这个行为的先后顺序:

// A. Single mock whose methods must be invoked in a particular order
List singleMock = mock(List.class);
// Use a single mock
singleMock.add("was added first");
singleMock.add("was added second");
// Create an inOrder verifier for a single mock
InOrder inOrder = inOrder(singleMock);
// Following will make sure that add is first called with "was added first, then with "was added second"
inOrder.verify(singleMock).add("was added first");
inOrder.verify(singleMock).add("was added second"); // B. Multiple mocks that must be used in a particular order
List firstMock = mock(List.class);
List secondMock = mock(List.class);
// Use mocks
firstMock.add("was called first");
secondMock.add("was called second");
// Create inOrder object passing any mocks that need to be verified in order
InOrder inOrder = inOrder(firstMock, secondMock);
// Following will make sure that firstMock was called before secondMock
inOrder.verify(firstMock).add("was called first");
inOrder.verify(secondMock).add("was called second");

存根连续调用

对于同一个方法,如果我们想让其在 多次调用 中分别 返回不同 的数值,那么就可以使用存根连续调用:

when(mock.someMethod("some arg"))
.thenThrow(new RuntimeException())
.thenReturn("foo"); // First call: throws runtime exception:
mock.someMethod("some arg");
// Second call: prints "foo"
System.out.println(mock.someMethod("some arg"));
// Any consecutive call: prints "foo" as well (last stubbing wins).
System.out.println(mock.someMethod("some arg"));

也可以使用下面更简洁的存根连续调用方法:

when(mock.someMethod("some arg")).thenReturn("one", "two", "three");

注意:存根连续调用要求必须使用链式调用,如果使用的是同个方法的多个存根配置,那么只有最后一个起作用(覆盖前面的存根配置)。

// All mock.someMethod("some arg") calls will return "two"
when(mock.someMethod("some arg").thenReturn("one")
when(mock.someMethod("some arg").thenReturn("two")

无返回值函数

对于 返回类型void 的方法,存根要求使用另一种形式的 when(Object) 函数,因为编译器要求括号内不能存在 void 方法。

例如,存根一个返回类型为 void 的方法,要求调用时抛出一个异常:

doThrow(new RuntimeException()).when(mockedList).clear();
// Following throws RuntimeException:
mockedList.clear();

监视真实对象

前面使用的都是 mock 出来一个对象。这样,当 没有配置/存根 其具体行为的话,结果就会返回 空类型。而如果使用 特务对象spy),那么对于 没有存根 的行为,它会调用 原来对象 的方法。可以把 spy 想象成局部 mock

List list = new LinkedList();
List spy = spy(list); // Optionally, you can stub out some methods:
when(spy.size()).thenReturn(100);
// Use the spy calls *real* methods
spy.add("one");
spy.add("two"); // Prints "one" - the first element of a list
System.out.println(spy.get(0));
// Size() method was stubbed - 100 is printed
System.out.println(spy.size());
// Optionally, you can verify
verify(spy).add("one");
verify(spy).add("two");

注意:由于 spy 是局部 mock,所以有时候使用 when(Object) 时,无法做到存根作用。此时,就可以考虑使用 doReturn() | Answer() | Throw() 这类方法进行存根:

List list = new LinkedList();
List spy = spy(list);
// Impossible: real method is called so spy.get(0) throws IndexOutOfBoundsException (the list is yet empty)
when(spy.get(0)).thenReturn("foo");
// You have to use doReturn() for stubbing
doReturn("foo").when(spy).get(0);

spy 并不是 真实对象代理。相反的,它对传递过来的 真实对象 进行 克隆。所以,对 真实对象 的任何操作,spy 对象并不会感知到。同理,对 spy 对象的任何操作,也不会影响到 真实对象

当然,如果使用 mock 进行对象的 局部 mock,通过 doCallRealMethod() | thenCallRealMethod() 方法也是可以的:

// You can enable partial mock capabilities selectively on mocks:
Foo mock = mock(Foo.class);
// Be sure the real implementation is 'safe'.
// If real implementation throws exceptions or depends on specific state of the object then you're in trouble.
when(mock.someMethod()).thenCallRealMethod();

Mocktio的局限

    不能 mock 静态方法;
    不能 mock 构造器;
    不能 mock equals()hashCode() 方法。

Mockito原理

对象生成原理肯定是动态代理,调用Mockito.mock生成mock代理类,首先是生成对应代理类class,如果多次调用Mockito.mock生成同样的mock代理类,肯定不会多次生成对应代理类class,而是在第一次生成之后保存到cache中,后续直接获取即可,对应代码如下:

// net.bytebuddy.TypeCache#findOrInsert
public Class<?> findOrInsert(ClassLoader classLoader, T key, Callable<Class<?>> lazy) {
Class<?> type = find(classLoader, key);
if (type != null) {
// 已缓存直接返回
return type;
} else {
return insert(classLoader, key, lazy.call());
}
}
// org.mockito.internal.creation.bytebuddy.TypeCachingBytecodeGenerator#mockClass
public <T> Class<T> mockClass(final MockFeatures<T> params) {
try {
ClassLoader classLoader = params.mockedType.getClassLoader();
return (Class<T>) typeCache.findOrInsert(classLoader,
new MockitoMockKey(params.mockedType, params.interfaces, params.serializableMode, params.stripAnnotations),
new Callable<Class<?>>() {
@Override
public Class<?> call() throws Exception {
// 动态生成代理类class
return bytecodeGenerator.mockClass(params);
}
}, BOOTSTRAP_LOCK);
} catch (IllegalArgumentException exception) {
//
}
}

动态生成代理类class对应方法:

org.mockito.internal.creation.bytebuddy.BytecodeGenerator#mockClass,如下:

public <T> Class<? extends T> mockClass(MockFeatures<T> features) {
String name = nameFor(features.mockedType);
DynamicType.Builder<T> builder =
byteBuddy.subclass(features.mockedType)
.name(name)
.ignoreAlso(isGroovyMethod())
.annotateType(features.stripAnnotations
? new Annotation[0]
: features.mockedType.getAnnotations())
// 方法处理器 DispatcherDefaultingToRealMethod
.implement(new ArrayList<Type>(features.interfaces))
.method(matcher)
.intercept(to(DispatcherDefaultingToRealMethod.class))
.transform(withModifiers(SynchronizationState.PLAIN))
.attribute(features.stripAnnotations
? MethodAttributeAppender.NoOp.INSTANCE
: INCLUDING_RECEIVER)
.method(isHashCode())
.intercept(to(MockMethodInterceptor.ForHashCode.class))
.method(isEquals())
.intercept(to(MockMethodInterceptor.ForEquals.class))
.serialVersionUid(42L)
.defineField("mockitoInterceptor", MockMethodInterceptor.class, PRIVATE)
.implement(MockAccess.class)
.intercept(FieldAccessor.ofBeanProperty());
if (features.serializableMode == SerializableMode.ACROSS_CLASSLOADERS) {
builder = builder.implement(CrossClassLoaderSerializableMock.class)
.intercept(to(MockMethodInterceptor.ForWriteReplace.class));
}
if (readReplace != null) {
builder = builder.defineMethod("readObject", void.class, Visibility.PRIVATE)
.withParameters(ObjectInputStream.class)
.throwing(ClassNotFoundException.class, IOException.class)
.intercept(readReplace);
}
ClassLoader classLoader = new MultipleParentClassLoader.Builder()
.append(features.mockedType)
.append(features.interfaces)
.append(currentThread().getContextClassLoader())
.append(MockAccess.class, DispatcherDefaultingToRealMethod.class)
.append(MockMethodInterceptor.class,
MockMethodInterceptor.ForHashCode.class,
MockMethodInterceptor.ForEquals.class).build(MockMethodInterceptor.class.getClassLoader());
if (classLoader != features.mockedType.getClassLoader()) {
assertVisibility(features.mockedType);
for (Class<?> iFace : features.interfaces) {
assertVisibility(iFace);
}
builder = builder.ignoreAlso(isPackagePrivate()
.or(returns(isPackagePrivate()))
.or(hasParameters(whereAny(hasType(isPackagePrivate())))));
}
return builder.make()
.load(classLoader, loader.resolveStrategy(features.mockedType, classLoader, name.startsWith(CODEGEN_PACKAGE)))
.getLoaded();
}

参考:

https://zhuanlan.zhihu.com/p/87523954

https://juejin.cn/post/6844903631137800206

单元测试Mockito框架的相关教程结束。

《单元测试Mockito框架.doc》

下载本文的Word格式文档,以方便收藏与打印。