单元测试中的Mock

Mock和Stub是单元测试工作中经常听到的名词。那么什么是Mocking?简而言之,就是创建一个受自己控制的实例(instance)或实现(implemention),替换被测代码中的依赖部分。

所谓受控的实例,是指依赖的行为是按自己所预期那样设计的,因而可以消除运行时的不确定性,解决测试用例运行时的环境依赖等问题,只关注核心代码逻辑的测试。

用一个实际业务系统来举例子,大多数的应用分为三层,分别是用户接口(User Interface),业务层(Business Layer)和数据接入层(Data Access Layer)。如下图所示。

在图中可以看到,业务层有3个依赖,分别是数据接入层和两个其他的服务。设想这是一个地图App,那么它们包括:

  1. 一个比如Mysql的数据库或其他NoSql的数据库,用来存储数据。
  2. 一个外部的服务,例如定位服务,提供经纬度信息。
  3. 一个外部的服务,例如交通信息服务,提供实时交通信息。

若要用单元测试来测试这样一个地图App的业务逻辑,除非先部署好这3个依赖,否则测试用例就运行不起来。

采用Mock方法可以应对这种情况,不管依赖服务是否已经运行起来。因为我们将使用自己设定的结果替换掉依赖的服务。

测试替身(Test doubles)的分类

Mock本质上是一种“测试替身”——这是一个技术术语。“测试双”本质上是指一个被等效的实际对象实例或依赖所替代的对象。

测试替身有多种分类,具体来说:

1. Facks(伪替身)

伪依赖是一个和真实依赖一样能运行的实例,唯一的不同是Fack位于本地系统。

例如,在测试中使用一个简单的集合数据结构,或者内存数据,来代替实际依赖的生产数据库。

2. Stubs(打桩)

打桩是使用一个预设的返回值代替调用实际依赖组件而返回的结果。

3. Spies(监视)

顾名思义,Spy是在调用实际依赖函数的时候,提供一种监视机制。这样就可以验证代码是否调到了某个函数,并拿到传入的参数进行验证。

4. Mocks(模拟)

Mock是一种特殊的对象实例,集合了打桩、监视的功能。既可以设定打桩,或者预设的返回值,也可以在事后验证mock对象的方法是否被正确调用到了。

例如,有一个报表生成器的函数,它在运行过程中向指定的地址发送电子邮件。由于我们不想发送实际的电子邮件,在测试期间,EmailService会被一次又一次地mock(发送电子邮件的电子邮件方法在被调用时被配置为不执行任何操作)。在测试结束时,我们只需要验证邮件服务确实调用到了我们mock的方法,并传进去了正确的邮箱地址。

Mock 框架

几乎所有的语言都提供了不同类型的mocking 框架。这里以Java的Mockito来举例。

假设我们要对一个应用程序进行单元测试,该应用程序计算一个学生在所有科目中的总分,并将其写入数据库。函数如下。

    public void calculateSumAndStore(String studentId, int[] scores) {int total = 0;for (int score : scores) {total = total + score;} // write total to DBdatabaseImpl.updateScores(studentId, total);}

我们要为方法calculateSumAndStore编写单元测试,但我们可能没有一个真正的数据库实现来存储总数。在这种情况下,我们将永远无法对该函数进行单元测试。

但是有了Mock之后,我们可以简单地传递一个用于数据库服务的Mock,并验证其余的逻辑。

测试代码的样例如下:

@Testpublic void calculateSumAndStore_withValidInput_shouldCalculateAndUpdateResultInDb() {// ArrangestudentScores = new StudentScoreUpdates(mockDatabase);int[] scores = {  60, 70, 90  };Mockito.doNothing().when(mockDatabase).updateScores("student1", 220);// ActstudentScores.calculateSumAndStore("student1", scores);// AssertMockito.verify(mockDatabase, Mockito.times(1)).updateScores("student1", 220);
}

我们在被测代码的父类中提供了一个mock的数据库对象mockDatabase,并在第6行通过打桩设定了它的返回结果:

(Mockito.doNothing().when(mockDatabase).updateScores(“student1”, 220);)

其中的要点是:

  1. 需要给mock对象里可能被调用的函数设置打桩结果。
  2. 创建打桩时的输入参数可以是特定的,也可以是通用化的。在本例中,打桩函数的入参就是特定的student1” & 220,因为我们明确知道代码调用它时的参数。
  3. 在验证结果时,我们检验:
    1. mockDatabase.updateScores方法被调用了
    2. 调用时传入的参数分别是student1” 和220
    3. uodataScores方法总共被调用了1次

我们接着改变一下测试代码,将打桩的入参从特定的“student1”改为anyString()和anyInteger(). anyxxx()是Mockito提供的匹配器,表示该类型任意的值。

测试代码如下:

    @Testpublic void calculateSumAndStore_withValidInput_shouldCalculateAndUpdateResultInDb() {// ArrangestudentScores = new StudentScoreUpdates(mockDatabase);int[] scores = {60, 70, 90};Mockito.doNothing().when(mockDatabase).updateScores(anyString(), anyInt());// ActstudentScores.calculateSumAndStore("student1", scores);// AssertMockito.verify(mockDatabase, Mockito.times(1)).updateScores("student1", 220);}

这个时候,测试用例依旧是能通过的。

我们再改变一下代码,这次将验证语句中的值改掉。将220改成230.

    Testpublic void calculateSumAndStore_withValidInput_shouldCalculateAndUpdateResultInDb() {// ArrangestudentScores = new StudentScoreUpdates(mockDatabase);int[] scores = {60, 70, 90};Mockito.doNothing().when(mockDatabase).updateScores(anyString(), anyInt());// ActstudentScores.calculateSumAndStore("student1", scores);// AssertMockito.verify(mockDatabase, Mockito.times(1)).updateScores("student1", 230);}

这时测试用例就通不过了,报异常信息:

Argument(s) are different! Wanted:
mockDatabase.updateScores(“student1”, 230);
-> at com.mocking.sampleMocks.StudentScoreUpdatesUnitTests.calculateSumAndStore_withValidInput_shouldCalculateAndUpdateResultInDb(StudentScoreUpdatesUnitTests.java:37)

Actual invocation has different arguments:
mockDatabase.updateScores(“student1”, 220);

告诉我们“student1”, 230这样的组合参数没有被调到。

小结

以上通过一个简单而直观的例子展示了使用Mockito进行mock的方法。

虽然简单,但只要知道了Mockito,其他语言、其他大多数通过mock进行的单元测试,流程和原理都是类似的。Mockito为广泛的mock需求提供了大量高级配置/支持,使用依赖注入注入模拟实例,提供了Spies来监视真正的方法调用并验证调用结果。

示例完整代码

接口 IDatabase.java

public interface IDatabase {public void updateScores(String studentId, int total);
}

被测代码 StudentScoreUpdates.java

public class StudentScoreUpdates {public IDatabase databaseImpl;public StudentScoreUpdates(IDatabase databaseImpl) {this.databaseImpl = databaseImpl;}public void calculateSumAndStore(String studentId, int[] scores) {int total = 0;for (int score : scores) {total = total + score;}// write total to DBdatabaseImpl.updateScores(studentId, total);}
}

测试用例类 – StudentScoreUpdatesUnitTests.java

public class StudentScoreUpdatesUnitTests {@Mockpublic IDatabase mockDatabase;public StudentScoreUpdates studentScores;@BeforeEachpublic void beforeEach(){MockitoAnnotations.initMocks(this);}@Testpublic void calculateSumAndStore_withValidInput_shouldCalculateAndUpdateResultInDb(){
// ArrangestudentScores = new StudentScoreUpdates(mockDatabase);int[] scores = {60,70,90};Mockito.doNothing().when(mockDatabase).updateScores(anyString(), anyInt());// ActstudentScores.calculateSumAndStore("student1", scores);// AssertMockito.verify(mockDatabase, Mockito.times(1)).updateScores("student1", 230);}}

Mockito 框架用于单元测试相关推荐

  1. 使用Mockito进行Java单元测试

    Google在3月份推出了一个关于Android MVP架构的官方Sample,除MVP架构本身之外,在这个Sample中配备了完善的单元测试用例,这对学习如何在Android中进行单元测试具有极高的 ...

  2. Android测试最新框架,Android单元测试-常见的方案比较

    前言 本文将介绍在Android Studio中,android单元测试的介绍和实现.相关代码托管在github上的AndroidJunitDemo中,涉及到的用例代码收集于google官方提供的测试 ...

  3. Mockito框架学习

    Mockito框架学习 在做单元测试的时候,有的时候用到的一些类,我们构造起来不是那么容易,比如HttpRequest,或者说某个Service依赖到了某个Dao,想构造service还得先构造dao ...

  4. 使用Spock框架进行单元测试

    阅读目录 2.1.1.单元测试是什么 2.1.2.单元测试的定位 2.2.1.单元测试的资料不够全 2.2.2.单元测试难以理解和维护 2.2.3.单元测试难以去除依赖 3.1.1.groovy 3. ...

  5. sql初学者指南_使用tSQLt框架SQL单元测试面向初学者

    sql初学者指南 tSQLt is a powerful, open source framework for SQL Server unit testing. In this article, we ...

  6. Mockito一个用于Java开发的伟大的模拟框架

    原文地址:https://www.codeproject.com/articles/516360/mockito-a-great-mock-framework-for-java-developmen ...

  7. boost::test模块命名函数参数框架的单元测试

    boost::test模块命名函数参数框架的单元测试 实现功能 C++实现代码 实现功能 boost::test模块命名函数参数框架的单元测试 C++实现代码 #define BOOST_TEST_M ...

  8. Python使用pytest框架进行单元测试

    在使用pytest框架进行单元测试之前,VsCode中需要已经安装Python插件,并且当前项目的环境中需要引入pytest依赖库: poetry add pytest 默认情况下单元测试功能是关闭的 ...

  9. 实验三,基于Unittest框架的单元测试

    实验三,基于Unittest框架的单元测试 一,实验目的 (1) 掌握单元测试技术,并按单元测试的要求设计测试用例. (2) 能熟练应用功能性测试技术进行测试用例设计: (3) 能熟练应用结构性测试技 ...

最新文章

  1. 13委托和事件在观察者模式中的应用
  2. jsp 学习 第2步 - tag 使用
  3. Java如何查看死锁?
  4. MATLAB数据分析3
  5. iOS逆向工程(简单利用dumpdecrypted给ipa砸壳)
  6. python 判断文件夹或文件是否存在
  7. nginx查看配置文件nginx.conf路径
  8. MICROSOFT REPORT VIEWER 2012之无法加载相关的dll
  9. Spark(Shuffle)
  10. maven 打包javadoc乱码解决方案
  11. c性能大容量cket_5千左右预算,既轻薄(高颜值)又高性能的笔记本推荐(畅玩LOL、CF、DNF、流放之路、梦幻西游)...
  12. iphone手机如何修改Apple ID密码
  13. 【NLP-笔记】开篇
  14. 流行和声(5)minor7和弦
  15. 微服务入门到入土(07)-分布式搜索ElasticSearch
  16. nginx gzip压缩
  17. 【python】20行代码实现有道翻译api接口调用
  18. 2022春季中国餐厅周来了!集结16大城市600余家高端食府饕餮飨宴
  19. 单片机音频节奏灯_如何用单片机做出用音乐节奏来控制LED灯?
  20. mac 可以连上网,但是自带浏览器和谷歌浏览器打不开网页

热门文章

  1. JAVA学习从软件工程导论课自动出题软件编程项目开始
  2. C语言函数大全--f开头的函数(下)
  3. git强制覆盖分支代码 A branch -- B branch
  4. [2021年最新]国产时序性数据TDenige入门
  5. vue-tree-chart 组织架构-树形图-流程图(含鼠标右击事件)
  6. 项目经理如何让自己更强大,执行力很关键
  7. 【剑指 Offer_15】二进制中1的个数_PythonJava_逐位相与解法
  8. 微服务与Spring Cloud简介
  9. 备战Noip2018模拟赛3(B组) T2 Dance 开场舞蹈
  10. 一个学计算机的打字速度慢,小学初中学生打字速度慢的原因及解决办法