Mockito - Java 开发的优秀模拟框架。
Mockito 是一个用起来很棒的模拟框架。它让你用简洁的 API 编写出优雅的测试。
引言
本文将介绍模拟框架的一些基本概念、为什么应该使用它,并通过一个简单的 Java 应用 Mockito 的示例进行演示。
模拟的概念
在软件开发之外,“mock
” 这个词的意思是模仿或仿效。因此,“mock
” 可以被看作是一个替身、一个冒充者,或者在与软件开发相关的语境中最常被称为一个“fake
”(假的)。
Fake 对象经常被用作被测试类依赖项的替身。
术语与定义 |
依赖(Dependency) – 当应用程序中的一个类为了执行其预期功能而依赖于另一个类时,就称为依赖。依赖通常存储在被依赖类中的实例变量里。 |
被测试类(Class Under Test) – 在编写单元测试时,“单元”通常指单个类,特别是指正在编写测试的那个类。因此,被测试类就是正在被测试的应用程序类。 |
为什么模拟?
当我们学习编程时,我们的对象通常是独立的。任何“hello world”程序都不会依赖于外部类(除了 System.out),在我们学习语言的过程中编写的许多其他类也是如此。然而,在现实世界中,软件是有依赖的。我们有依赖于服务的 Action 类,有依赖于数据访问对象(DAO)的服务,以此类推。
单元测试的理念是我们希望在不测试依赖项的情况下测试我们的代码。这种测试允许你验证被测试的代码是否正常工作,而不管其依赖项。理论上,如果我编写的代码按设计工作,并且我的依赖项也按设计工作,那么它们应该能按设计协同工作。下面的代码将是这个示例:
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。
模拟对象背后的概念是,我们想要创建一个对象来取代真实的对象。这个模拟对象会期望在特定参数下调用某个方法,当这种情况发生时,它将返回一个预期的结果。
关键的模拟概念有哪些?
在模拟方面,你只需要关心 3 件事:存根(stubbing)、设置期望(setting expectations)和验证(verifying)。 有些单元测试场景不涉及这些,有些只涉及存根,还有些则涉及设置期望和验证。
存根(Stubbing)
存根是指告诉你的 fake 对象在与之交互时如何表现的过程。你通常可以存根公共属性(带有 getter 和/或 setter 的属性)和公共方法。
在存根方法时,你通常有很多选择。你可能希望返回一个特定值、抛出一个错误或派发一个事件。此外,你可能希望指示该方法根据其调用方式(例如,通过匹配传递给该方法的参数的类型或值)表现得不同。
如果这听起来工作量很大,那可能是,但通常并非如此。许多模拟框架的一个很棒的功能是,你不需要存根 void 方法。你也不必存根在测试执行期间未被调用或未被访问的任何函数或属性。
设置期望(Setting expectations)
Fake 对象的一个关键特性是能够告诉 fake 对象在测试运行时你期望发生什么。例如,你可能期望某个特定函数被调用恰好 3 次。你可能期望它永远不会被调用。你可能期望它至少被调用两次,但不超过 5 次。你可能期望它使用特定类型的参数或特定值进行调用,或者以上任何组合。可能性是无限的。
设置期望是告诉你的 fake 对象你期望对其发生什么的的过程。请记住,因为它是 fake 对象,所以实际上什么都不会发生。但是,你的被测试类并不知道。从它的角度来看,它调用了该函数,并期望它做了任何它应该做的事情。
据我所知,大多数模拟框架都允许你创建接口或公共类的模拟对象。你并不局限于只能模拟接口。
验证(Verifying)
设置期望和验证是相辅相成的。设置期望是在调用被测试类上的函数(们)之前进行的。验证是在之后进行的。所以,首先你设置期望,然后你验证你的期望是否被满足。
从单元测试的角度来看,如果你的期望没有得到满足,单元测试就会失败。例如,如果你设置了对 `ILoginService.login` 函数的期望,即它应该用特定的用户名和密码被调用一次,但它在测试执行期间从未被调用,那么 fake 对象将不会验证,测试就应该失败。
模拟有什么好处?
你可以提前创建测试;测试驱动开发 (TDD)
这是其中一个更有力的好处。如果你创建了一个 Mock,你可以在服务创建之前编写服务测试,从而让你能够在开发过程中将测试添加到你的自动化环境中。换句话说,服务模拟使你能够使用测试驱动开发。
团队可以并行工作
这与上面类似;为不存在的代码创建测试。但上一点是为编写测试的开发人员准备的,这一点是为测试团队准备的。当没有什么可测试时,团队如何开始创建测试?模拟它并针对 Mock 编写测试!这意味着 QA 团队可以在服务准备好进行测试时,拥有一个完整的测试套件;我们不会因为一个团队在等待另一个团队完成而出现停滞。这使得模拟在经济上的论证尤其有力。
你可以创建概念验证或演示。
由于 Mock(明智地制作)成本效益很高,Mock 可以用来创建概念验证、线框图,或者用于演示你正在考虑构建的东西。这非常强大,为做出关于是否继续进行开发项目以及最重要的实际设计决策提供了良好的基础。
你可以为无法访问的资源编写测试
这是其中一个好处,它不属于实际好处的范畴,而是作为一种救命稻草。有没有想过要测试或使用某个服务,却被告知该服务在防火墙后面,而防火墙不能为你打开,或者你没有权限使用它?当你遇到这种情况时,一个放在可访问位置(包括你本地计算机上)的 MockService 可以是救命的。
Mock 可以交付给客户
在某些情况下,出于某些原因,你不能允许外部源(如合作伙伴或客户)访问你的测试系统。这些原因可能是访问安全、信息敏感性,或者仅仅是测试环境可能无法 24/7 全天候访问。在这种情况下;你如何为你的合作伙伴或客户提供一个测试系统来进行开发或测试?一个简单的解决方案是提供一个 mock,可以从你的网络或你的客户自己的网络中提供。soapUI mock 非常容易部署,它可以运行在 soapUI 中,也可以导出为 .WAR 文件并放置在你选择的 Java 服务器中。
你可以隔离系统
有时你想测试你系统的一部分而不让系统的其他部分影响它。这是因为其他系统会给测试数据带来“噪音”,使从收集到的数据中得出良好结论变得更加困难。使用 mocks,你可以移除所有依赖项,模拟除你需要精确测试的那个系统之外的所有系统。在进行隔离模拟时,这些模拟可以被做得非常简单但可靠、快速且可预测。这为你提供了一个测试环境,在该环境中,你已经消除了所有随机行为,拥有可重复的模式,并且可以很好地监控特定的系统。
Mockito 框架
Mockito 是一个开源的 Java 测试框架,根据 MIT 许可证发布。
Mockito 通过允许开发人员在不预先建立期望的情况下验证被测试系统 (SUT) 的行为来区别于其他模拟框架。[4] 对模拟对象的一个批评是测试代码与被测试系统之间的耦合更紧密。[5] 由于 Mockito 试图通过消除期望的规范来消除“期望-运行-验证”模式[6],因此耦合会减少或最小化。这种区别性特征的结果是更简单的测试代码,应该更容易阅读和修改。
你可以验证交互:
// mock creation
List mockedList = mock(List.class);
<span class="Apple-tab-span" style="white-space: pre;">
</span>// using mock object
mockedList.add("one");
mockedList.clear();
// selective and explicit vertification
verify(mockedList).add("one");
verify(mockedList).clear();
或者存根方法调用
// you can mock concrete class, not only interfaces
LinkedList mockedList = mock(LinkedList.class);
<span class="Apple-tab-span" style="white-space: pre;">
</span>// stubbing - before execution
when(mockedList.get(0)).thenReturn("first");
<span class="Apple-tab-span" style="white-space: pre;">
</span>// following prints "first"
System.out.println(mockedList.get(0));
<span class="Apple-tab-span" style="white-space: pre;">
</span>// following prints "null" because get(999) was not stubbed.
System.out.println(mockedList.get(999));
一个简单的 Java 代码示例,使用了 Mockito
没有模拟框架
使用 Mockito 框架
步骤 1:在 Eclipse 中创建一个 Maven 项目
将 pom.xml
定义为如下:
<?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 源代码
类 Person.java
package vn.com.enclave.phatbeo.ut.mockito.demo;
/**
* @author Phat (Phillip) H. VU <vuhongphat@hotmail.com>
*
*/
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 <vuhongphat@hotmail.com>
*
*/
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 <vuhongphat@hotmail.com> * */ 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:添加单元测试类。
然后,开始为 PersonService.java
类编写单元测试用例
假设,类 PersionServiceTest.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 <vuhongphat@hotmail.com>
*
*/
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 );
}
}
兴趣点
+ 模拟框架概述。
+ 为什么我们在 Java 开发的测试中使用 Mockito。
参考文献
http://java.dzone.com/articles/the-concept-mocking
http://en.wikipedia.org/wiki/Mockito
http://code.google.com/p/mockito/
历史
首次发布于 2012 年 12 月 26 日