单元测试的目标和挑战
单元测试的思路是在不涉及依赖关系的情况下测试代码(隔离性),所以测试代码与其他类或者系统的关系应该尽量被消除。
一个可行的消除方法是替换掉依赖类(测试替换),也就是说我们可以使用替身来替换掉真正的依赖对象。
mock测试
Mock 测试就是在测试过程中,对于某些不容易构造(如 HttpServletRequest 必须在Servlet 容器中才能构造出来)或者不容易获取比较复杂的对象(如 JDBC 中的ResultSet 对象),用一个虚拟的对象(Mock 对象)来创建以便测试的测试方法。
Mock 最大的功能是帮你把单元测试的耦合分解开,如果你的代码对另一个类或者接口有依赖,它能够帮你模拟这些依赖,并帮你验证所调用的依赖的行为。
比如一段代码有这样的依赖:
当我们需要测试A类的时候,如果没有 Mock,则我们需要把整个依赖树都构建出来,而使用 Mock 的话就可以将结构分解开,像下面这样:
Mock 对象使用范畴
真实对象具有不可确定的行为,产生不可预测的效果(如:天气预报) :
- 真实对象很难被创建的
- 真实对象的某些行为很难被触发
- 真实对象实际上还不存在的(和其他开发小组或者和新的硬件打交道)等等
关键步骤
- 使用一个接口来描述这个对象
- 在产品代码中实现这个接口
- 在测试代码中实现这个接口
- 在被测试代码中只是通过接口来引用对象,所以它不知道这个引用的对象是真实对象,还是 Mock 对象。
用Mock测试你的代码
测试驱动的开发(Test Driven Design, TDD)要求我们先写单元测试,再写实现代码。在写单元测试的过程中,一个很普遍的问题是,要测试的类会有很多依赖,这些依赖的类/对象/资源又会有别的依赖,从而形成一个大的依赖树,要在单元测试的环境中完整地构建这样的依赖,是一件很困难的事情。
所幸,我们有一个应对这个问题的办法:Mock。简单地说就是对测试的类所依赖的其他类和对象,进行mock - 构建它们的一个假的对象,定义这些假对象上的行为,然后提供给被测试对象使用。被测试对象像使用真的对象一样使用它们。用这种方式,我们可以把测试的目标限定于被测试对象本身,就如同在被测试对象周围做了一个划断,形成了一个尽量小的被测试目标。
Mock的框架有很多,最为知名的一个是Mockito,这是一个开源项目,使用广泛。
Java Mock 测试
目前,在 Java 阵营中主要的 Mock 测试工具有 Mockito,JMock,EasyMock 等。
Mockito 特性
- Mockito 是美味的 Java 单元测试 Mock 框架。
- 大多 Java Mock 库如 EasyMock 或 JMock 都是 expect-run-verify (期望-运行-验证)方式,你常常被迫查看无关的交互
- Mockito 则使用更简单,更直观的方法:在执行后的互动中提问。
- Mockito 无需准备昂贵的前期启动。他们的目标是透明的,让开发人员专注于测试选定的行为。
- Mockito 拥有的非常少的 API,所有开始使用 Mockito,几乎没有时间成本。因为只有一种创造 mock 的方式。只要记住,在执行前 stub,而后在交互中验证。你很快就会发现这样 TDD java 代码是多么自然。
- 可以 mock 具体类而不单止是接口
- 一点注解语法糖 -
@Mock
- 干净的验证错误是 - 点击堆栈跟踪,看看在测试中的失败验证;点击异常的原因来导航到代码中的实际互动。堆栈跟踪总是干干净净。
- 允许灵活有序的验证(例如:你任意有序
verify
,而不是每一个单独的交互) - 支持“详细的用户号码的时间”以及“至少一次”验证
- 灵活的验证或使用参数匹配器的 stub (
anyObject()
,anyString()
或refEq()
用于基于反射的相等匹配) - 允许创建自定义的参数匹配器或者使用现有的 hamcrest 匹配器
先睹为快
// 创建mock对象
List mockedList = Mockito.mock(List.class);
// 设置mock对象的行为 - 当调用其get方法获取第0个元素时,返回"one"
Mockito.when(mockedList.get(0)).thenReturn("one");
// 使用mock对象 - 会返回前面设置好的值"one",即便列表实际上是空的
String str = mockedList.get(0);
Assert.assertTrue("one".equals(str));
Assert.assertTrue(mockedList.size() == 0);
// 验证mock对象的get方法被调用过,而且调用时传的参数是0
Mockito.verify(mockedList).get(0);
基本分析
让我们仔细想想看,下面这个代码:
// 设置mock对象的行为 - 当调用其get方法获取第0个元素时,返回"one"
Mockito.when(mockedList.get(0)).thenReturn("one");
如果按照一般代码的思路去理解,是要做这么一件事:
- 调用mockedList.get方法,传入0作为参数,然后得到其返回值(一个object),然后再把这个返回值传给when方法,然后针对when方法的返回值,调用thenReturn。好像有点不通?mockedList.get(0)的结果,语义上是mockedList的一个元素,这个元素传给when是表示什么意思?所以,我们不能按照寻常的思路去理解这段代码。
- 实际上这段代码要做的是描述这么一件事情:当mockedList的get方法被调用,并且参数的值是0的时候,返回”one”。
- 很不寻常,对吗?如果用平常的面向对象的思想来设计API来做同样的事情,估计结果是这样的:
Mockito.returnValueWhen("one", mockedList, "get", 0);
第一个参数描述要返回的结果,第二个参数指定mock对象,第三个参数指定mock方法,后面的参数指定mock方法的参数值。这样的代码,更符合我们看一般代码时候的思路。
-
但是,把上面的代码跟Mockito的代码进行比较,我们会发现,我们的代码有几个问题:
- 不够直观
- 对重构不友好
- 第二点尤其重要。想象一下,如果我们要做重构,把get方法改名叫fetch方法,那我们要把”get”字符串替换成”fetch”,而字符串替换没有编译器的支持,需要手工去做,或者查找替换,很容易出错。而Mockito使用的是方法调用,对方法的改名,可以用编译器支持的重构来进行,更加方便可靠。
实现分析
Mock对象这件事情,本质上是一个Proxy模式的应用。
Proxy模式说的是,在一个真实对象前面,提供一个proxy对象,所有对真实对象的调用,都先经过proxy对象,然后由proxy对象根据情况,决定相应的处理,它可以直接做一个自己的处理,也可以再调用真实对象对应的方法。Proxy对象对调用者来说,可以是透明的,也可以是不透明的。
Java本身提供了构建Proxy对象的API:Java Dynamic Proxy API。Mockito就是用Java提供的Dynamic Proxy API来实现的。
仔细分析,就会发现,示例代码最难理解的部分是:
- 建立Mock对象(proxy对象)
- 配置mock方法(指定其在什么情况下返回什么值)
// 设置mock对象的行为 - 当调用其get方法获取第0个元素时,返回"one"
Mockito.when(mockedList.get(0)).thenReturn("one");
- 基本思想:
Mockito使用什么方式来传递信息? —— 不是用方法的返回值,而是用某种全局的变量。当get方法被调用的时候(调用的实际上是proxy对象的get方法),代码实际上保存了被调用的方法名(get),以及调用时候传递的参数(0),然后等到thenReturn方法被调用的时候,再把”one”保存起来,这样,就有了构建一个stub方法所需的所有信息,就可以构建一个stub方法了。 - 源码分析:
public Object handle(Invocation invocation) throws Throwable {
if (invocationContainerImpl.hasAnswersForStubbing()) {
...
}
...
InvocationMatcher invocationMatcher = matchersBinder.bindMatchers(
mockingProgress.getArgumentMatcherStorage(),
invocation
);
mockingProgress.validateState();
// if verificationMode is not null then someone is doing verify()
if (verificationMode != null) {
...
}
// prepare invocation for stubbing invocationContainerImpl.setInvocationForPotentialStubbing(invocationMatcher);
OngoingStubbingImpl<T> ongoingStubbing =
new OngoingStubbingImpl<T>(invocationContainerImpl);
mockingProgress.reportOngoingStubbing(ongoingStubbing);
...
}
注意第1行,第6-9行,可以看到方法调用的信息(invocation)对象被用来构造invocationMatcher对象,然后在第19-21行,invocationMatcher对象最终传递给了ongoingStubbing对象。完成了stub信息的保存。
小结
通过以上的分析我们可以看到,Mockito在设计时实际上有意地使用了方法的“副作用”,在返回值之外,还保存了方法调用的信息,进而在最后利用这些信息,构建出一个mock。而这些信息的保存,是对Mockito的用户完全透明的。
这是一个经典的“反模式”的使用案例。“模式”告诉我们,在设计方法的时候,应该避免副作用,一个方法在被调用时候,除了return返回值之外,不应该产生其他的状态改变,尤其不应该有“意料之外”的改变。但Mockito完全违反了这个原则,Mockito的静态方法Mockito.anyString(), mockInstance.method(), Mockito.when(), thenReturn(),这些方法,在背后都有很大的“副作用” —— 保存了调用者的信息,然后利用这些信息去完成任务。这就是为什么Mockito的代码一开始会让人觉得奇怪的原因,因为我们平时不这样写代码。
然而,作为一个Mocking框架,这个“反模式”的应用实际上是一个好的设计。就像我们前面看到的,它带来了非常简单的API,以及编译安全,可重构等优良特性。违反直觉的方法调用,在明白其原理和一段时间的熟悉之后,也显得非常的自然了。设计的原则,终究是为设计目标服务的,原则在总结出来之后,不应该成为僵硬的教条,根据需求灵活地应用这些原则,才能达成好的设计。在这方面,Mockito堪称一个经典案例。
基本用法
verify some behaviour!
//mock creation
List mockedList = mock(List.class);
//using mock object
mockedList.add("one");
mockedList.clear();
//verification
verify(mockedList).add("one");
verify(mockedList).clear();
stubbing smth
//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);
- By default, for all methods that return a value, a mock will return either null, a primitive/primitive wrapper value, or an empty collection, as appropriate. For example 0 for an int/Integer and false for a boolean/Boolean.
- Stubbing can be overridden: for example common stubbing can go to fixture setup but the test methods can override it. Please note that overridding stubbing is a potential code smell that points out too much stubbing
- Once stubbed, the method will always return a stubbed value, regardless of how many times it is called.
- Last stubbing is more important - when you stubbed the same method with the same arguments many times. Other words: the order of stubbing matters but it is only meaningful rarely, e.g. when stubbing exactly the same method calls or sometimes when argument matchers are used, etc.
Argument matchers
Mockito verifies argument values in natural java style: by using an equals() method. Sometimes, when extra flexibility is required then you might use argument matchers:
//mock creation
List mockedList = mock(List.class);
//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(i -> (Integer)i > 10))).thenReturn(true);
//following prints "element"
System.out.println(mockedList.get(999));
System.out.println(mockedList.add(30));
System.out.println(mockedList.add(60));
//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(i -> (Integer)i > 50));
Warning on argument matchers:
If you are using argument matchers, all arguments have to be provided by matchers.
The following example shows verification but the same applies to stubbing:
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.
Verifying exact number of invocations exact_verification/ at least x/ / never
//mock creation
List mockedList = mock(List.class);
//using mock
mockedList.add("once");
mockedList.add("twice");
mockedList.add("twice");
mockedList.add("three times");
mockedList.add("three times");
mockedList.add("three times");
//following 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");
times(1) is the default. Therefore using times(1) explicitly can be omitted.
Stubbing void methods with exceptions
doThrow(new RuntimeException()).when(mockedList).clear();
//following throws RuntimeException:
mockedList.clear();
Find redundant invocations
//using mocks
mockedList.add("one");
mockedList.add("two");
verify(mockedList).add("one");
//following verification will fail
verifyNoMoreInteractions(mockedList);
annotation mock
- Minimizes repetitive mock creation code.
- Makes the test class more readable.
- Makes the verification error easier to read because the field name is used to identify the mock.
更多官方详细使用示例参考文末Ref 使用
静态引用
如果在代码中静态引用了org.mockito.Mockito.*;那就可以直接调用静态方法和静态变量而不用创建对象,譬如直接调用 mock() 方法。
这个对于Mockito很好用,单是一般我们会配置包的明确引用,不是*。
除了上面所说的使用 mock() 静态方法外,Mockito 还支持通过 @Mock 注解的方式来创建 mock 对象。
如果你使用注解,则必须要实例化 mock 对象。
Mockito 在遇到使用注解的字段的时候,会调用MockitoAnnotations.initMocks(this) 来初始化该 mock 对象。另外也可以通过使用@RunWith(MockitoJUnitRunner.class)来达到相同的效果。
import static org.mockito.Mockito.*;
public class MockitoTest {
@Mock
MyDatabase databaseMock; // (1)
@Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); //(2)
@Test
public void testQuery() {
ClassToTest t = new ClassToTest(databaseMock); //(3)
boolean check = t.query("* from t");// (4)
assertTrue(check); //(5)
verify(databaseMock).query("* from t"); //(6)
}
}
// =================说明
- 1. 告诉 Mockito 模拟 databaseMock 实例
- 2. Mockito 通过 @mock 注解创建 mock 对象
- 3. 使用已经创建的mock初始化这个类
- 4. 在测试环境下,执行测试类中的代码
- 5. 使用断言确保调用的方法返回值为 true
- 6. 验证 query 方法是否被 MyDatabase 的 mock 对象调用
配置 mock
当我们需要配置某个方法的返回值的时候,Mockito 提供了链式的 API 供我们方便的调用
when(….).thenReturn(….)
可以被用来定义当条件满足时函数的返回值,如果你需要定义多个返回值,可以多次定义。当你多次调用函数的时候,Mockito 会根据你定义的先后顺序来返回值(stack原理)。Mocks 还可以根据传入参数的不同来定义不同的返回值。譬如说你的函数可以将anyString 或者 anyInt作为输入参数,然后定义其特定的放回值。
@Test
public void test1() {
// 创建 mock
MyClass test = Mockito.mock(MyClass.class);
// 自定义 getUniqueId() 的返回值
when(test.getUniqueId()).thenReturn(43);
// 在测试中使用mock对象
assertEquals(test.getUniqueId(), 43);
}
// 返回多个值
@Test
public void testMoreThanOneReturnValue() {
Iterator i= mock(Iterator.class);
when(i.next()).thenReturn("Mockito").thenReturn("rocks");
String result=i.next()+" "+i.next();
// 断言
assertEquals("Mockito rocks", result);
}
// 如何根据输入来返回值
@Test
public void testReturnValueDependentOnMethodParameter() {
Comparable c= mock(Comparable.class);
when(c.compareTo("Mockito")).thenReturn(1);
when(c.compareTo("Eclipse")).thenReturn(2);
// 断言
assertEquals(1,c.compareTo("Mockito"));
}
// 如何让返回值不依赖于输入
@Test
public void testReturnValueInDependentOnMethodParameter() {
Comparable c= mock(Comparable.class);
when(c.compareTo(anyInt())).thenReturn(-1);
// 断言
assertEquals(-1 ,c.compareTo(9));
}
// 根据参数类型来返回值
@Test
public void testReturnValueInDependentOnMethodParameter() {
Comparable c= mock(Comparable.class);
when(c.compareTo(isA(Todo.class))).thenReturn(0);
// 断言
Todo todo = new Todo(5);
assertEquals(todo ,c.compareTo(new Todo(1)));
}
// 对于无返回值的函数,我们可以使用doReturn(…).when(…).methodCall来获得类似的效果。
// 如我们想在调用某些无返回值函数的时候抛出异常,那么可以使用doThrow 方法
@Test(expected=IOException.class)
public void testForIOException() {
// 创建并配置 mock 对象
OutputStream mockStream = mock(OutputStream.class);
doThrow(new IOException()).when(mockStream).close();
// 使用 mock
OutputStreamWriter streamWriter= new OutputStreamWriter(mockStream);
streamWriter.close();
}
验证 mock 对象方法是否被调用(即所谓的行为验证)
Mockito 会跟踪 mock 对象里面所有的方法和变量。所以我们可以用来验证函数在传入特定参数的时候是否被调用。这种方式的测试称行为测试,行为测试并不会检查函数的返回值,而是检查在传入正确参数时候函数是否被调用。
@Test
public void testVerify() {
// 创建并配置 mock 对象
MyClass test = Mockito.mock(MyClass.class);
when(test.getUniqueId()).thenReturn(43);
// 调用mock对象里面的方法并传入参数为12
test.testing(12);
test.getUniqueId();
test.getUniqueId();
// 查看在传入参数为12的时候方法是否被调用
verify(test).testing(Matchers.eq(12));
// 方法是否被调用两次,默认是1次
verify(test, times(2)).getUniqueId();
// 其他用来验证函数是否被调用的方法
verify(mock, never()).someMethod("never called");
verify(mock, atLeastOnce()).someMethod("called at least once");
verify(mock, atLeast(2)).someMethod("called at least twice");
verify(mock, times(5)).someMethod("called five times");
verify(mock, atMost(3)).someMethod("called at most 3 times");
}
使用 Spy 封装 java 对象
@Spy或者spy()方法可以被用来封装 java 对象。被封装后,除非特殊声明(打桩 stub),否则都会真正的调用对象里面的每一个方法
.
// Lets mock a LinkedList
List list = new LinkedList();
List spy = spy(list);
// 可用 doReturn() 来打桩
doReturn("foo").when(spy).get(0);
// 下面代码不生效
// 真正的方法会被调用
// 将会抛出 IndexOutOfBoundsException 的异常,因为 List 为空
when(spy.get(0)).thenReturn("foo");
@InjectMocks 在 Mockito 中进行依赖注入
// 假定我们有 ArticleManager 类
public class ArticleManager {
private User user;
private ArticleDatabase database;
ArticleManager(User user) {
this.user = user;
}
void setDatabase(ArticleDatabase database) { }
}
@RunWith(MockitoJUnitRunner.class)
public class ArticleManagerTest {
@Mock ArticleCalculator calculator;
@Mock ArticleDatabase database;
@Most User user;
@Spy private UserProvider userProvider = new ConsumerUserProvider();
// 这个类会被 Mockito 构造,而类的成员方法和变量都会被 mock 对象所代替
@InjectMocks private ArticleManager manager;// (1)
@Test public void shouldDoSomething() {
// 假定 ArticleManager 有一个叫 initialize() 的方法被调用了
// 使用 ArticleListener 来调用 addListener 方法
manager.initialize();
// 验证 addListener 方法被调用
verify(database).addListener(any(ArticleListener.class));
}
}
捕捉参数Captor
ArgumentCaptor类允许我们在verification期间访问方法的参数。得到方法的参数后我们可以使用它进行测试。
public class MockitoTests {
@Rule public MockitoRule rule = MockitoJUnit.rule();
@Captor
private ArgumentCaptor> captor;
@Test
public final void shouldContainCertainListItem() {
List asList = Arrays.asList("someElement_test", "someElement");
final List mockedList = mock(List.class);
mockedList.addAll(asList);
verify(mockedList).addAll(captor.capture());
final List capturedArgument = captor.>getValue();
assertThat(capturedArgument, hasItem("someElement"));
}
}
Mockito的限制
而下面三种数据类型则不能够被测试
- final classes
- anonymous classes
- primitive types
实例:使用 Mockito 创建一个 mock 对象
// 创建一个Twitter API 的例子
public interface ITweet {
String getMessage();
}
public class TwitterClient {
public void sendTweet(ITweet tweet) {
String message = tweet.getMessage();
// send the message to Twitter
}
}
// 模拟 ITweet 的实例
@Test
public void testSendingTweet() {
TwitterClient twitterClient = new TwitterClient();
ITweet iTweet = mock(ITweet.class);
when(iTweet.getMessage()).thenReturn("Using mockito is great");
twitterClient.sendTweet(iTweet);
}
// 验证方法调用
@Test
public void testSendingTweet() {
TwitterClient twitterClient = new TwitterClient();
ITweet iTweet = mock(ITweet.class);
when(iTweet.getMessage()).thenReturn("Using mockito is great");
twitterClient.sendTweet(iTweet);
// 验证 getMessage() 方法至少调用一次。
verify(iTweet, atLeastOnce()).getMessage();
}
模拟静态方法
因为 Mockito 不能够 mock 静态方法,因此我们可以使用 Powermock。
// 模拟了 NetworkReader 的依赖
@RunWith( PowerMockRunner.class )
@PrepareForTest( NetworkReader.class )
public class MyTest {
final class NetworkReader {
public static String getLocalHostname() {
String hostname = "";
try {
InetAddress addr = InetAddress.getLocalHost();
// Get hostname
hostname = addr.getHostName();
} catch ( UnknownHostException e ) {
}
return hostname;
}
}
// 测试代码
@Test
public void testSomething() {
mockStatic( NetworkUtil.class );
when( NetworkReader.getLocalHostname() ).andReturn( "localhost" );
}
// 有时候我们可以在静态方法周围包含非静态的方法来达到和 Powermock 同样的效果。
class FooWraper {
void someMethod() {
Foo.someStaticMethod()
}
}
Ref:
官网:http://site.mockito.org/
介绍:http://www.infoq.com/cn/articles/mockito-design/
使用:
https://juejin.im/entry/578f11aec4c971005e0caf82
http://static.javadoc.io/org.mockito/mockito-core/2.19.0/org/mockito/Mockito.html
git:https://github.com/mockito/mockito
其他:
https://waylau.com/mockito-quick-start/