本文将介绍模拟测试框架Mockito的一些基础概念, 介绍该框架的优点,讲解应用Mockito的Java示例.
在软件开发的世界之外, “mock”一词是指模仿或者效仿. 因此可以将“mock”理解为一个替身,替代者. 在软件开发中提及”mock”,通常理解为模拟对象或者Fake.
译者注:mock等多代表的是对被模拟对象的抽象类,你可以把fake理解为mock的实例。不知道这样说准不准确:)
Fake通常被用作被测类的依赖关系的替代者.
名次定义
依赖关系 – 依赖关系是指在应用程序中一个类基于另一个类来执行其预定的功能.依赖关系通常都存在于所依赖的类的实例变量中.
被测类 – 在编写单元测试的时候, “单元”一词通常代表一个单独的类及为其编写的测试代码. 被测类指的就是其中被测试的类.
为什么需要模拟?
在我们一开始学编程时,我们所写的对象通常都是独立的. hello world之类的类并不依赖其他的类(System.out除外),也不会操作别的类.但实际上,软件中是充满依赖关系的.我们会基于service类写操作类,而service类又是基于数据访问类(DAOs)的,依次下去.
单元测试的思路就是我们想在不涉及依赖关系的情况下测试代码. 这种测试可以让你无视代码的依赖关系去测试代码的有效性.核心思想就是如果代码按设计正常工作,并且依赖关系也正常,那么他们应该会同时工作正常.
下面的代码就是这样的例子:
import java.util.ArrayList;
public class Counter {
public Counter() {
}
public int count(ArrayList items) {
int results = 0;
for(Object curItem : items) {
results ++;
}
return results;
}
}
如你所见,上面的例子十分简单,但它阐明了要点.当你想要测试count方法时,你会针对count方法本身如何工作去写测试代码. 你不会去测试ArrayList是否正常工作,因为你默认它已经被测过并且工作正常.你唯一的目标就是测试对ArrayList的使用.
在谈到模拟时,你只需关心三样东西: 设置测试数据,设定预期结果,验证结果.一些单元测试方案根本就不涉及这些,有的只涉及设置测试数据,有的只涉及设定预期结果和验证.
Stubbing就是告诉fake当与之交互时执行何种行为过程。通常它可以用来提供那些测试所需的公共属性(像getters和setters)和公共方法。
当谈到stubbing方法,通常你有一系列的选择。或许你希望返回一个特殊的值,抛出一个错误或者触发一个事件,此外,你可能希望指出方法被调用时的不同行为(即通过传递匹配的类型或者参数给方法)。
这咋一听起来工作量很大,但通常并非这样。许多mocking框架的一个重要功能就是你不需要提供stub 的实体方法,也不用在执行测试期间stub那些未被调用的方法或者未使用的属性。
Fake的一个关键的特性就是当你用它进行模拟测试时你能够告诉它你预期的结果。例如,你可以要求一个特定的函数被准确的调用3次,或不被调用,或调用至少两次但不超过5次,或者需要满足特定类型的参数、特定值和以上任意的组合的调用。可能性是无穷的。
通过设定预期结果告诉fake你期望发生的事情。因为它是一个模拟测试,所以实际上什么也没发生。但是,对于被测试的类来说,它并无法区分这种情况。所以fake能够调用函数并让它做它该做的。
值得注意的是,大多数模拟框架除了可以创建接口的模拟测试外,还可以创建公有类的模拟测试。
设置预期和验证预期是同时进行的。设置预期在调用测试类的函数之前完成,验证预期则在它之后。所以,首先你设定好预期结果,然后去验证你的预期结果是否正确。
在一个单元测试中,如果你设定的预期没有得到满足,那么这个单元测试就是失败了。例如,你设置预期结果是 ILoginService.login函数必须用特定的用户名和密码被调用一次,但是在测试中它并没有被调用,这个fake没被验证,所以测试失败。
这是个最大的好处吧。如果你创建了一个Mock那么你就可以在service接口创建之前写Service Tests了,这样你就能在开发过程中把测试添加到你的自动化测试环境中了。换句话说,模拟使你能够使用测试驱动开发。
这类似于上面的那点;为不存在的代码创建测试。但前面讲的是开发人员编写测试程序,这里说的是测试团队来创建。当还没有任何东西要测的时候测试团队如何来创建测试呢?模拟并针对模拟测试!这意味着当service借口需要测试时,实际上QA团队已经有了一套完整的测试组件;没有出现一个团队等待另一个团队完成的情况。这使得模拟的效益型尤为突出了。
由于Mocks非常高效,Mocks可以用来创建一个概念证明,作为一个示意图,或者作为一个你正考虑构建项目的演示程序。这为你决定项目接下来是否要进行提供了有力的基础,但最重要的还是提供了实际的设计决策。
这个好处不属于实际效益的一种,而是作为一个必要时的“救生圈”。有没有遇到这样的情况?当你想要测试一个service接口,但service需要经过防火墙访问,防火墙不能为你打开或者你需要认证才能访问。遇到这样情况时,你可以在你能访问的地方使用MockService替代,这就是一个“救生圈”功能。
在有些情况下,某种原因你需要允许一些外部来源访问你的测试系统,像合作伙伴或者客户。这些原因导致别人也可以访问你的敏感信息,而你或许只是想允许访问部分测试环境。在这种情况下,如何向合作伙伴或者客户提供一个测试系统来开发或者做测试呢?最简单的就是提供一个mock,无论是来自于你的网络或者客户的网络。soapUI mock非常容易配置,他可以运行在soapUI或者作为一个war包发布到你的java服务器里面。
有时,你希望在没有系统其他部分的影响下测试系统单独的一部分。由于其他系统部分会给测试数据造成干扰,影响根据数据收集得到的测试结论。使用mock你可以移除掉除了需要测试部分的系统依赖的模拟。当隔离这些mocks后,mocks就变得非常简单可靠,快速可预见。这为你提供了一个移除了随机行为,有重复模式并且可以监控特殊系统的测试环境。
Mockito 是一个基于MIT协议的开源java测试框架。
Mockito区别于其他模拟框架的地方主要是允许开发者在没有建立“预期”时验证被测系统的行为。对于mock对象的一个评价是测试系统的测试代码是一个高耦合的,由于 Mockito试图通过移除“期望规范”去除expect-run-verify(期望验证模式)的模式,因此在耦合度上有所降低。这样的结果是简化了测试代码,使他更易读和修改了。
// 模拟创建
List mockedList = mock(List.class);
// 使用模拟对象
mockedList.add("one");
mockedList.clear();
// 验证选择性和显式调用
verify(mockedList).add("one");
verify(mockedList).clear();
// 你不仅可以模拟接口,任何具体类都行
LinkedList mockedList = mock(LinkedList.class);
// 执行前准备测试数据
when(mockedList.get(0)).thenReturn("first");
// 接着打印"first"
System.out.println(mockedList.get(0));
// 因为get(999)未对准备数据,所以下面将打印"null".
System.out.println(mockedList.get(999));
使用Mock框架
使用Mockito框架
步骤 1: 在Eclipse中创建一个Maven项目
定义thepom.xmlas文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<pre><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>vn.com.phatbeo.ut.mockito.demo</groupId>
<artifactId>demoMockito</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>demoMockito</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<sourceDirectory>src</sourceDirectory>
<testSourceDirectory>test</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.1</version>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.8.5</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
步骤 2: 添加java源码
ClassPerson.java
package vn.com.enclave.phatbeo.ut.mockito.demo;
/** * @author Phat (Phillip) H. VU <[email protected]> * */
public class Person {
private final Integer personID;
private final String personName;
public Person( Integer personID, String personName )
{
this.personID = personID;
this.personName = personName;
}
public Integer getPersonID()
{
return personID;
}
public String getPersonName()
{
return personName;
}
}
接口类 PersonDAO.java
package vn.com.enclave.phatbeo.ut.mockito.demo;
/** * @author Phat (Phillip) H. VU <[email protected]> * */
public interface PersonDao {
public Person fetchPerson( Integer personID );
public void update( Person person );
}
类 PersonService.java
package vn.com.enclave.phatbeo.ut.mockito.demo;
/** * @author Phat (Phillip) H. VU <[email protected]> * */
public class PersonService {
private final PersonDao personDao;
public PersonService( PersonDao personDao )
{
this.personDao = personDao;
}
public boolean update( Integer personId, String name )
{
Person person = personDao.fetchPerson( personId );
if( person != null )
{
Person updatedPerson = new Person( person.getPersonID(), name );
personDao.update( updatedPerson );
return true;
}
else
{
return false;
}
}
}
步骤 3: 添加单元测试类.
接下来为classPersonService.java编写单元测试用例
可以设计classPersionServiceTest.java为如下:
package vn.com.enclave.phatbeo.ut.mockito.demo.test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
/** * @author Phat (Phillip) H. VU <[email protected]> * */
public class PersonServiceTest {
@Mock
private PersonDao personDAO;
private PersonService personService;
@Before
public void setUp()
throws Exception
{
MockitoAnnotations.initMocks( this );
personService = new PersonService( personDAO );
}
@Test
public void shouldUpdatePersonName()
{
Person person = new Person( 1, "Phillip" );
when( personDAO.fetchPerson( 1 ) ).thenReturn( person );
boolean updated = personService.update( 1, "David" );
assertTrue( updated );
verify( personDAO ).fetchPerson( 1 );
ArgumentCaptor<Person> personCaptor = ArgumentCaptor.forClass( Person.class );
verify( personDAO ).update( personCaptor.capture() );
Person updatedPerson = personCaptor.getValue();
assertEquals( "David", updatedPerson.getPersonName() );
// asserts that during the test, there are no other calls to the mock object.
verifyNoMoreInteractions( personDAO );
}
@Test
public void shouldNotUpdateIfPersonNotFound()
{
when( personDAO.fetchPerson( 1 ) ).thenReturn( null );
boolean updated = personService.update( 1, "David" );
assertFalse( updated );
verify( personDAO ).fetchPerson( 1 );
verifyZeroInteractions( personDAO );
verifyNoMoreInteractions( personDAO );
}
}
http://java.dzone.com/articles/the-concept-mocking
http://en.wikipedia.org/wiki/Mockito
http://code.google.com/p/mockito
http://liuzhijun.iteye.com/blog/1512780/