Mockito:一个强大的用于Java开发的模拟测试框架
https://blog.csdn.net/zhoudaxia/article/details/33056093
介绍
本文将介绍模拟测试框架Mockito的一些基础概念, 介绍该框架的优点,讲解应用Mockito的Java示例。
模拟(Mock)的概念
在软件开发的世界之外, "mock"一词是指模仿或者效仿。因此可以将“mock”理解为一个替身,替代者。在软件开发中提及"mock",通常理解为模拟对象或者fake。
译者注:mock等多代表的是对被模拟对象的抽象类,你可以把fake理解为mock的实例。不知道这样说准不准确:)
Fake通常被用作被测类的依赖关系的替代者.。
名词定义 |
依赖关系 – 依赖关系是指在应用程序中一个类基于另一个类来执行其预定的功能。依赖关系通常都存在于所依赖的类的实例变量中。 |
被测类 – 在编写单元测试的时候,“单元”一词通常代表一个单独的类及为其编写的测试代码。被测类指的就是其中被测试的类。 |
为什么需要模拟?
在我们一开始学编程时,我们所写的对象通常都是独立的。hello world之类的类并不依赖其他的类(System.out除外),也不会操作别的类。但实际上软件中是充满依赖关系的。我们会基于service类写操作类,而service类又是基于数据访问类(DAOs)的,依次下去。
图1 类的依赖关系
单元测试的思路就是我们想在不涉及依赖关系的情况下测试代码。这种测试可以让你无视代码的依赖关系去测试代码的有效性。核心思想就是如果代码按设计正常工作,并且依赖关系也正常,那么他们应该会同时工作正常。
下面的代码就是这样的例子:
- 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 (桩)
Stubbing就是告诉fake当与之交互时执行何种行为过程。通常它可以用来提供那些测试所需的公共属性(像getters和setters)和公共方法。
当谈到stubbing方法,通常你有一系列的选择。或许你希望返回一个特殊的值,抛出一个错误或者触发一个事件,此外,你可能希望指出方法被调用时的不同行为(即通过传递匹配的类型或者参数给方法)。
这咋一听起来工作量很大,但通常并非这样。许多mocking框架的一个重要功能就是你不需要提供stub 的实体方法,也不用在执行测试期间stub那些未被调用的方法或者未使用的属性。
设置预期
Fake的一个关键的特性就是当你用它进行模拟测试时你能够告诉它你预期的结果。例如,你可以要求一个特定的函数被准确的调用3次,或不被调用,或调用至少两次但不超过5次,或者需要满足特定类型的参数、特定值和以上任意的组合的调用。可能性是无穷的。
通过设定预期结果告诉fake你期望发生的事情。因为它是一个模拟测试,所以实际上什么也没发生。但是,对于被测试的类来说,它并无法区分这种情况。所以fake能够调用函数并让它做它该做的。
值得注意的是,大多数模拟框架除了可以创建接口的模拟测试外,还可以创建公有类的模拟测试。
验证预期结果
设置预期和验证预期是同时进行的。设置预期在调用测试类的函数之前完成,验证预期则在它之后。所以,首先你设定好预期结果,然后去验证你的预期结果是否正确。
在一个单元测试中,如果你设定的预期没有得到满足,那么这个单元测试就是失败了。例如,你设置预期结果是 ILoginService.login函数必须用特定的用户名和密码被调用一次,但是在测试中它并没有被调用,这个fake没被验证,所以测试失败。
模拟的好处是什么?
提前创建测试; TDD(测试驱动开发)
这是个最大的好处吧。如果你创建了一个Mock那么你就可以在service接口创建之前写Service Tests了,这样你就能在开发过程中把测试添加到你的自动化测试环境中了。换句话说,模拟使你能够使用测试驱动开发。
团队可以并行工作
这类似于上面的那点;为不存在的代码创建测试。但前面讲的是开发人员编写测试程序,这里说的是测试团队来创建。当还没有任何东西要测的时候测试团队如何来创建测试呢?模拟并针对模拟测试!这意味着当service借口需要测试时,实际上QA团队已经有了一套完整的测试组件;没有出现一个团队等待另一个团队完成的情况。这使得模拟的效益型尤为突出了。
你可以创建一个验证或者演示程序。
由于Mocks非常高效,Mocks可以用来创建一个概念证明,作为一个示意图,或者作为一个你正考虑构建项目的演示程序。这为你决定项目接下来是否要进行提供了有力的基础,但最重要的还是提供了实际的设计决策。
为无法访问的资源编写测试
这个好处不属于实际效益的一种,而是作为一个必要时的“救生圈”。有没有遇到这样的情况?当你想要测试一个service接口,但service需要经过防火墙访问,防火墙不能为你打开或者你需要认证才能访问。遇到这样情况时,你可以在你能访问的地方使用MockService替代,这就是一个“救生圈”功能。
Mock 可以分发给用户
在有些情况下,某种原因你需要允许一些外部来源访问你的测试系统,像合作伙伴或者客户。这些原因导致别人也可以访问你的敏感信息,而你或许只是想允许访问部分测试环境。在这种情况下,如何向合作伙伴或者客户提供一个测试系统来开发或者做测试呢?最简单的就是提供一个mock,无论是来自于你的网络或者客户的网络。soapUI mock非常容易配置,他可以运行在soapUI或者作为一个war包发布到你的java服务器里面。
隔离系统
有时,你希望在没有系统其他部分的影响下测试系统单独的一部分。由于其他系统部分会给测试数据造成干扰,影响根据数据收集得到的测试结论。使用mock你可以移除掉除了需要测试部分的系统依赖的模拟。当隔离这些mocks后,mocks就变得非常简单可靠,快速可预见。这为你提供了一个移除了随机行为,有重复模式并且可以监控特殊系统的测试环境。
Mockito 框架
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));
一个使用Mockito框架的简单Java代码示例
图2 不使用Mock框架
图3 使用Mockito框架
步骤 1: 在IDE中创建一个普通的Java项目
在Eclipse、NetBeans或IntelliJ IDEA中创建一个普通的Java项目。
步骤 2: 添加java源码
类Person.java:
- package mockitodemo;
- 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 mockitodemo;
- public interface PersonDao
- {
- public Person fetchPerson( Integer personID );
- public void update( Person person );
- }
类PersonService.java:
- package mockitodemo;
- 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创建单元测试用例。我们使用JUnit 4.x和Mockito 1.9.5。可以设计测试用例类PersionServiceTest.java为如下,代码中有详细注释说明:
- package mockitodemo;
- import org.junit.After;
- import org.junit.AfterClass;
- import org.junit.Before;
- import org.junit.BeforeClass;
- import org.junit.Test;
- import static org.junit.Assert.*;
- import org.mockito.Mock;
- import org.mockito.MockitoAnnotations;
- import org.mockito.ArgumentCaptor;
- import static org.mockito.Mockito.*;
- /**
- * PersonService的单元测试用例
- *
- * @author jackzhou
- */
- public class PersonServiceTest {
- @Mock
- private PersonDao personDAO; // 模拟对象
- private PersonService personService; // 被测类
- public PersonServiceTest() {
- }
- @BeforeClass
- public static void setUpClass() {
- }
- @AfterClass
- public static void tearDownClass() {
- }
- // 在@Test标注的测试方法之前运行
- @Before
- public void setUp() throws Exception {
- // 初始化测试用例类中由Mockito的注解标注的所有模拟对象
- MockitoAnnotations.initMocks(this);
- // 用模拟对象创建被测类对象
- personService = new PersonService(personDAO);
- }
- @After
- public void tearDown() {
- }
- @Test
- public void shouldUpdatePersonName() {
- Person person = new Person(1, "Phillip");
- // 设置模拟对象的返回预期值
- when(personDAO.fetchPerson(1)).thenReturn(person);
- // 执行测试
- boolean updated = personService.update(1, "David");
- // 验证更新是否成功
- assertTrue(updated);
- // 验证模拟对象的fetchPerson(1)方法是否被调用了一次
- verify(personDAO).fetchPerson(1);
- // 得到一个抓取器
- ArgumentCaptor<Person> personCaptor = ArgumentCaptor.forClass(Person.class);
- // 验证模拟对象的update()是否被调用一次,并抓取调用时传入的参数值
- verify(personDAO).update(personCaptor.capture());
- // 获取抓取到的参数值
- Person updatePerson = personCaptor.getValue();
- // 验证调用时的参数值
- assertEquals("David", updatePerson.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);
- // 验证模拟对象的fetchPerson(1)方法是否被调用了一次
- verify(personDAO).fetchPerson(1);
- // 验证模拟对象是否没有发生任何交互
- verifyZeroInteractions(personDAO);
- // 检查模拟对象上是否还有未验证的交互
- verifyNoMoreInteractions(personDAO);
- }
- /**
- * Test of update method, of class PersonService.
- */
- @Test
- public void testUpdate() {
- System.out.println("update");
- Integer personId = null;
- String name = "Phillip";
- PersonService instance = new PersonService(new PersonDao() {
- @Override
- public Person fetchPerson(Integer personID) {
- System.out.println("Not supported yet.");
- return null;
- }
- @Override
- public void update(Person person) {
- System.out.println("Not supported yet.");
- }
- });
- boolean expResult = false;
- boolean result = instance.update(personId, name);
- assertEquals(expResult, result);
- // TODO review the generated test code and remove the default call to fail.
- fail("The test case is a prototype.");
- }
- }
这里setUpClass()、tearDownClass()、setUp()、tearDown()称为测试夹具(Fixture),就是测试运行程序(test runner)在运行测试方法之前进行初始化、或之后进行回收资源的工作。JUnit 4之前是通过setUp、tearDown方法完成。在JUnit 4中,仍然可以在每个测试方法运行之前初始化字段和配置环境,当然也是通过注解完成。在JUnit 4中,通过@Before标注setUp方法;@After标注tearDown方法。在一个测试类中,甚至可以使用多个@Before来注解多个方法,这些方法都是在每个测试之前运行。说明一点,一个测试用例类可以包含多个打上@Test注解的测试方法,在运行时,每个测试方法都对应一个测试用例类的实例。@Before是在每个测试方法运行前均初始化一次,同理@Ater是在每个测试方法运行完毕后均执行一次。也就是说,经这两个注解的初始化和注销,可以保证各个测试之间的独立性而互不干扰,它的缺点是效率低。另外,不需要在超类中显式调用初始化和清除方法,只要它们不被覆盖,测试运行程序将根据需要自动调用这些方法。超类中的@Before方法在子类的@Before方法之前调用(与构造函数调用顺序一致),@After方法是子类在超类之前运行。
这里shouldUpdatePersonName()、shouldNotUpdateIfPersonNotFound()和testUpdate()都是测试PersonService的update()方法,它依赖于PersonDao接口。前两者使用了模拟测试。testUpdate()则没有使用模拟测试。下面是测试结果:
图4 测试结果点击打开链接
可以看出,使用模拟测试的两个测试成功了,没有使用模拟测试的testUpdate()失败。对于模拟测试,在测试用例类中要先声明依赖的各个模拟对象,在setUp()中用MockitoAnnotations.initMocks()初始化所有模拟对象。在进行模拟测试时,要先设置模拟对象上方法的返回预期值,执行测试时会调用模拟对象上的方法,因此要验证这些方法是否被调用,并且传入的参数值是否符合预期。对于testUpdate()测试,我们需要自己创建测试PersonService.update()所需的所有PersonDao数据,因为我们只知道公开的PersonDao接口,其具体实现类(比如从数据库中拿真实的数据,或写入到数据库中)可能由另一个团队在负责,以适配不同的数据库系统。这样的依赖关系无疑使单元测试比较麻烦,而要拿真正PersonDao实现来进行测试,那也应该是后期集成测试的任务,把不同的组件集成到一起在真实环境中测试。有了模拟测试框架,就可以最大限度地降低单元测试时的依赖耦合性。
关注点
+ Mock框架是什么.
+ 为什么要在测试中使用Mockito.
参考
http://java.dzone.com/articles/the-concept-mocking
http://en.wikipedia.org/wiki/Mockito
http://code.google.com/p/mockito
本文翻译自:http://www.codeproject.com/Articles/516360/Mockito-a-great-mock-framework-for-Java-developmen ,并做了一些修改。
Mockito:一个强大的用于Java开发的模拟测试框架的更多相关文章
- 10个用于Web开发的最好 Python 框架
Python 是一门动态.面向对象语言.其最初就是作为一门面向对象语言设计的,并且在后期又加入了一些更高级的特性.除了语言本身的设计目的之外,Python标准 库也是值得大家称赞的,Python甚至还 ...
- 纯 Java 开发 WebService 调用测试工具(wsCaller.jar)
注:本文来自hacpai.com:Tanken的<纯 Java 开发 WebService 调用测试工具(wsCaller.jar)>的文章 基于 Java 开发的 WebService ...
- 珠联壁合地设天造|M1 Mac os(Apple Silicon)基于vscode(arm64)配置搭建Java开发环境(集成web框架Springboot)
原文转载自「刘悦的技术博客」https://v3u.cn/a_id_194 也许有人从未听说过Python,但是不会有人没听说过Java,它作为一个拥有悠久历史的老牌编程语言,常年雄踞TIOBE编程语 ...
- 【转】Java学习---10个测试框架介绍
[原文]https://www.toutiao.com/i6594302925458113027/ JAVA 程序员需要用到 10 个测试框架和库 Java 程序员需要用到十大单元测试和自动化集成测试 ...
- iOS开发中的测试框架
转载作者:@crespoxiao 我们为什么要用测试框架呢?当然对项目开发有帮助了,但是业内现状是经常赶进度,所以TDD还是算了吧,BDD就测测数据存取和重要环节,这很重要,一次性跑完测试单元检查接口 ...
- iOS开发中的测试框架 (转载)
作者:CrespoXiao授权 地址:http://www.jianshu.com/p/7e3f197504c1 我们为什么要用测试框架呢?当然对项目开发有帮助了,但是业内现状是经常赶进度,所以T ...
- 网上的JAVA语言的某个测试框架
https://github.com/wenchengyao/testLJTT.git 使用maven打包,mvn clean install 在运行的时候,java -jar testLJTT.ja ...
- [0403]学习一个——苟(简单Java开发)
学习一个--苟 1. 开发目的 拜读了某神犇的blog,感到了自身深深的不足.蒟蒻如我,决定提高一蛤自身的姿势水平,学习一个,使用Java重写用GreatestLanguage写的某小说网站的抓取器. ...
- Java开发中模拟接口工具moco的使用
场景 在开发中需要依赖一些接口,比如需要请求一个返回Json数据的接口,但是返回Json数据的接口要么是没搭建,要么是交互比较复杂. 此时,就可以使用moco来模拟接口返回接口数据,以便开发和测试工作 ...
随机推荐
- Educational Codeforces Round 3 D. Gadgets for dollars and pounds 二分+前缀
D. Gadgets for dollars and pounds time limit per test 2 seconds memory limit per test 256 megabytes ...
- python (协程)生产者,消费者
#coding=utf- import gevent from gevent.queue import Queue, Empty import time tasks = Queue(maxsize=) ...
- 给大一新生学习c程序的一些建议的一些建议
这是一篇给刚学习c程序的学弟们的一篇日志.如果想学好c程序,以及不想走太多弯路,希望能看一下这篇文章,如果说基础较好,或者说已经是大二,大三,这篇文章不会有什么帮助. 刚转到软件工程系,加了几个新生群 ...
- python web py安装与简单使用
web.py是一个轻量级的python web框架,简单而且功能强大.相对flask和Django,web.py更适合初学者来学习和了解web开发的基础知识. 安装: pip install we ...
- NativeWindow_02_DialogBoxParam_VC6
PS: 代码参考于<<Windows环境下32位汇编语言程序设计.chm>>,第5章,"5.4 对 话 框(2)" . ZC: 与CreateWindow( ...
- scala函数式编程(一)
scala函数编程特点: 1.Scala函数使用命名参数: 即函数参数传递的实参与函数名相对应,与函数位置不对应. object Test { def main(args: Array[String] ...
- 最短路径遍历所有的节点 Shortest Path Visiting All Nodes
2018-10-06 22:04:38 问题描述: 问题求解: 本题要求是求遍历所有节点的最短路径,由于本题中是没有要求一个节点只能访问一次的,也就是说可以访问一个节点多次,但是如果表征两次节点状态呢 ...
- (转)C# 单例模式
文章1: 一.多线程不安全方式实现 public sealed class SingleInstance { private static SingleInstance inst ...
- Can't push you anymore...
为什么我们不趁着年轻去冒险? 等我们准备好,也许都已经被生活冲淡了激情. Go to different places,to meet different people. To try, to fin ...
- canvas获取摄像头的图像
aaarticlea/png;base64,iVBORw0KGgoAAAANSUhEUgAAAugAAAF3CAIAAAB44CnMAAAgAElEQVR4nOy92a4kx5am1y8lQAIkCI