2.1 良好的单元测试——定义

我们已经了解了程序员需要单元测试, 下面我们来给单元测试作一个完整的定义:

  • 定义: 单元测试是一段自动执行的代码, 它调用被测类或被测方法, 然后验证关于被测类或被测方法逻辑行为的假设确实成立. 单元测试几乎总是用单元测试框架(unit testing framework)来写就的, 单元测试是易于写就、执行快速、完全自动化、值得依赖、易于阅读并易于维护的.

这个定义有点长, 但是它却包含了大量重要信息:

  • 单元测试的测试重点是被测类或被测方法的逻辑行为. 所谓"逻辑行为", 指的是含有诸如判断、循环、选择、计算或其他决策过程的代码.
  • 单元测试框架是辅助程序员写就单元测试的利器.
  • 单元测试的代码本身, 同被测代码一样, 也应该是值得依赖、易于阅读并易于维护的.

有了单元测试的定义, 我们来看看有关单元测试的几个基本概念, 这些基本概念会在后面的章节中反复出现.

  • 被测类(Class Under Test)和被测方法(Method Under Test): 顾名思义, 就是测试代码所操练的类或方法.
  • 测试类(Test Fixture)和测试方法(Test Method): 负责操练被测类和被测方法. Test Method一般都是Test Fixture的成员函数.
  • 测试运行器(Test Runner): 负责自动执行测试类中的测试方法. Test Runner可以是命令行界面的, 也可以是GUI的.

2.2 进行单元测试的核心技术和核心手法

2.2.1 核心技术

前面已经讲到, 单元测试所针对的目标是"单个"类或"单个"方法(这里的"方法"指C++中的自由函数). 这表明我们在单元测试中要做的主要任务是创建出被测类的对象, 并调用该对象的被测方法. 但是我们都知道, 一个类几乎不可能完全不依赖于其他类. 这种类之间的依赖会导致我们无法顺利地将一个被测类纳入单元测试覆盖这下, 因为单元测试需要的是对被测类这一个类的测试, 而不是同时测试被测类和它的合作者类.

因此, 我们在把ClassUnderTest纳入单元测试时, 也就必须先把ClassUnderTest与CollaboratorClass之间的依赖"打破", 这个过程称为"解依赖"(dependency-breaking). 解依赖就是进行单元测试的核心技术. 解依赖的目标是希望能把不可控的CollaboratorClass替换成由我们控制的伪合作者类(FakeCollaboratorClass), 并使被测类能方便地选择是依赖于真实的合作者类还是伪合作者类. 对于产品代码, 被测类依赖的是真实的合作者类, 而在单元测试中, 被测类依赖的是由我们控制的伪合作者.

在单元测试代码中使用可控的FakeCollaboratorClass, 这给我们带来了两个便利:

  • 我们可以通过FakeCollaboratorClass向ClassUnderTest返回任何我们指定的结果.
  • 我们可以通过FakeCollaboratorClass来感知ClassUnderTest所做的动作.

这实际上就是FakeCollaboratorClass的两种表现形式:

  • Stub: 用于向ClassUnderTest返回我们指定的结果.
  • Mock: 用于感知ClassUnderTest的行为.

我们明白了"解依赖"是单元测试的核心技术. 那么具体怎样实现解依赖呢? 下面我们就来介绍4种相关的核心手法, 其中前2种与CollaboratorClass有关, 后2种与ClassUnderTest有关, 这4种手法对于解决绝大多数的解依赖问题都适用.

2.2.2 "接口提取"和"Virtual and Override"

"接口提取"手法是对CollaboratorClass提取出一个抽象接口CollaboratorService, 然后让CollaboratorClass和FakeCollaboratorClass都去实现这个接口, 而ClassUnderTest则由直接依赖CollaboratorClass转向依赖于抽象接口CollaboratorService, 如下图所示.

实际上, "接口提取"手法是一种非常好的手法, 它使得我们的代码遵循"依赖抽象原则", 遵循这个原则的软件具有较好的灵活性, 这是具有可测试性的软件也具有较好的设计的一个佐证.

而"Virtual and Override"手法则是使CollaboratorClass中的被依赖方法成为virtual, 然后让FakeCollaboratorClass去公有继承CollaboratorClass, 并且override那些虚函数, 从而替换掉 CollaboratorClass中的行为. 这种手法如下图所示.

总体上讲, 我们推荐优先使用"接口提取"手法, 因为这将使代码遵循"依赖抽象原则", 从而使软件更好地应对今后的变化. 但是, "Virtual and Override"方法也是有其一席之地的, 我们后面会看到例子.

2.2.3 "参数注入"和"Extract and Override"

在ClassUnderTest这边, 对CollaboratorClass的依赖的产生方式也可以划分成两类:

  • 依赖是通过方法参数传入的, 这种形式的依赖被称为"参数注入"式依赖(parameter injection dependency). 参数注入式依赖是一种耦合度较低的依赖产生形式, 因此对ClassUnderTest的影响不大, 一般最多只需要把方法的签名由直接依赖CollaboratorClass改成依赖接口CollaboratorService.
  • 依赖是在被测方法的方法体内部产生的, 这种依赖被称为"隐藏式"依赖(hidden dependency). 隐藏式依赖有多种表现形式:

o 直接创建CollaboratorClass对象作为局部变量或成员变量.

o 通过一个工厂方法来产生CollaboratorClass对象.

o 通过一个工厂类来产生CollaboratorClass对象.

隐藏式依赖是一种耦合程度较高的依赖, 因此是我们着重需要"打破"的依赖. 一种方法是把隐藏式依赖转变成参数注入式依赖, 我们将在后面的小节中看到这种方法的应用. 而另一种方法, 则是使用"Extract and Override"手法, 即: 我们给ClassUnderTest引入一个virtual的工厂成员函数, 来返回一个CollaboratorClass对象的引用, 然后对ClassUnderTest派生出一个子类, 在该子类中override这个工厂成员函数, 让它返回一个FakeCollaboratorClass对象的引用, 如下图所示.

这里的TestingClassUnderTest被称为"测试用子类"(Testing Subclass). 这时, 在Test Fixture中被实例化的其实就是测试用子类, 而不是被测类本身.

以上我们研究了进行单元测试所需要的核心技术, 以及4种最常用的核心手法, 这些手法足以应付绝大多数的情况, 但是, 仍然有一些特殊情况需要我们特别注意, 我们从下一节开始, 以Q&A的形式, 讨论这些特殊情况.

2.3 应付构造函数中的隐藏式依赖

当ClassUnderTest中出现隐藏式依赖时, 最常用的有两种手法来打破这种依赖. 我们分别来看一下.

2.3.1 转变成参数注入式依赖

对于像C#和Java这样的语言, 由于它们支持在一个构造函数中去调用另一个构造函数, 因此可以很方便地增加一个构造函数, 把隐藏式依赖转变成参数注入式依赖. 下面的UML图阐释了这种手法.

而对于C++, 由于它没有提供在构造函数中调用另一个构造函数的功能, 因此通常的作法就是把公共的的初始化代码放入一个init()私有方法中去, 让不同的构造函数去调用init()方法.

2.3.2 使用"调包方法"

还可以考虑给ClassUndertTest引入一个"调包方法", 使得测试类和测试方法可以很方便地将合作者类"调包"成伪合作者类. 这里的"调包方法"本质上就是一个setter方法, 但是为了表明这个特殊的setter方法只应该被测试类和测试方法调用, 我们一般给调包方法命名为SupersedeCollaborator(). 下图就是一个演示.

这里必须要提醒的是, 在C++中使用这个手法时必须注意在"调包方法"中不能引起资源泄漏, 也就是说, 必须先释放掉原有的合作者对象, 再以伪合作者替代之.

2.4 怎样测试ClassUnderTest中的private方法?

如果要测试一个类的private方法, 答案的总体思路是: 适当打破访问权限. 也就是说, 我们可以把private方法声明成protected方法, 然后对ClassUnderTest进行派生, 得到一个"测试用子类", 在该"测试用子类"中声明一个public方法, 该方法是ClassUnderTest中的protected方法的代理. 这种手法可以用下图来表示.

2.4 应付"全局依赖"

所谓"全局依赖", 是指被测类或被测方法所依赖的合作者是一个"全局单件", 包括C#/Java/C++中的"单件类"(Singleton)和C++中的全局变量和自由方法(实际上, Singleton可视为全局变量在面向对象语言中的变种). 我们下面来看看怎么应对这些情况.

2.4.1 使用"封装全局引用"手法来解除对全局变量和自由方法的依赖

要打破对全局变量和自由方法的依赖, 其实基本思想就是: 把它们纳入到一个类中, 让它们成为一个类的成员变量和成员方法. 一旦把全局变量和自由函数封装进了某个类, 那么我们就可以继续利用2.2.2节提到的两种手法来引入伪合作者了.

2.4.2 使用"静态调包方法"来解除对单件类的依赖

单件类往往具有3个特点:

(1) 单件类的构造函数通常被设为private.

(2) 单件类具有一个静态成员变量, 该成员变量持有该类的唯一一个实例.

(3) 单件类具有一个静态方法, 用来提供对单件实例的访问, 通常该方法名叫getInstance().

由于单件类被设计成强制性地在整个程序中只有一份, 因此要伪造它比较困难. 我们推荐使用"静态调包方法"手法. 这个手法的本质是适当打破单件类的单件性约束, 把单件类的构造函数改为protected, 使我们能够从单件类派生出一个Fake子类, 然后为单件类引入一个静态调包函数SupersedeInstance(), 以允许单件类中的静态成员变量被"调包"成Fake子类对象. 下图表明了这一手法.

同样必须强调的是, 在C++中的使用这个手法的时候, 必须保证没有资源泄漏.

2.5 如果CollaboratorClass本身就处于继承体系中, 怎么办?

先设想一下CollaboratorClass本身存在基类的情况, 如下图所示.

如果使用"Virtual and Override"手法来伪造合作者类, 那么不存在任何问题, 我们可以用下面的图来表示.

另一方面, 如果想使用"接口提取"手法的话, 那么一种比较好的策略是使用"外覆类"(Wrapper Class), 如下图所示.

2.6 当CollaboratorClass是标准类库的一员时, 怎么办?

有些时候, 我们的被测类所依赖的是特定操作系统(Windows, Unix, 或Mac)、特定标准规范(.NET, 或J2EE)、特定函数库或类库(如POSIX API和Win32 API)及特定用户界面(CUI或者GUI)所提供的功能时, 这实际上是引入了对特定平台的依赖性, 而这往往都是在提示我们: 应该加入一个更高层次的抽象(也即一个间接层, indirection), 从而将这种对特定平台的依赖隐藏到这个抽象之后. 换句话说, 我们应该引入一个接口, 来使我们的代码具有平台无关性, 如下图所示.

2.7 怎样测试抽象接口?

假设我们的系统中定义了一个抽象接口ServiceInterface,系统中有两个类(分别是ServiceImpl1和ServiceImpl2)实现了这个接口。现在,我们希望为ServiceInteface抽象接口编写一个通用的测试类,这个测试类不仅能测试当前已经实现该接口的类,而且可以不加修改地应用于将来实现ServiceInteface接口的类。应该怎么办呢?下图展示了一种可能的方案。

上图中,ServiceInterfaceTestFixture测试类中的测试方法都是基于ServiceInterface来进行测试的,不依赖于其具体实现类。这样就保证了仅测试抽象接口所定义的行为。当将来系统引入ServiceInteface的新的实现类时,只需要从ServiceInterfaceTestFixture类再派生出一个新子类,并实现createServiceInstance()方法来创建相应的对象即可。

单元测试的基本概念和核心技法相关推荐

  1. 第二章 单元测试的基本概念和核心技法

    第二章 单元测试的基本概念和核心技法 2.1 良好的单元测试--定义 我们已经了解了程序员需要单元测试,下面我们来给单元测试作一个完整的定义: ● 定义: 单元测试是一段自动执行的代码,它调用被测类或 ...

  2. git基本概念与核心命令_Git:了解核心版本控制概念的初学者指南

    git基本概念与核心命令 Git is a vital tool in the toolbelt of any developer. Git是任何开发人员的重要工具. For example, jus ...

  3. 王阳明心学的基础概念和核心思想

    简介 王阳明(1472年-1529年),字仲弓,号阳明,是明代著名的哲学家.思想家和军事家.他的思想被称为"心学",是中国思想史上的重要流派之一. 心学核心概念 王阳明心学的基础概 ...

  4. 【数据挖掘】基于密度的聚类方法 - DBSCAN 方法 ( K-Means 方法缺陷 | 基于密度聚类原理及概念 | ε-邻域 | 核心对象 | 直接密度可达 | 密度可达 | 密度连接 )

    文章目录 I . K-Means 算法在实际应用中的缺陷 II . K-Means 初始中心点选择不恰当 III . K-Means 优点 与 弊端 IV . 基于密度的聚类方法 V . 基于密度的聚 ...

  5. 邹伟博士出书啦!——《强化学习》从基础概念、核心原理到应用案例(文末赠书)...

    强化学习日渐流行,作为当今社会最热门的研究课题之一,其关注度正与日俱增.强化学习是机器学习的一个分支,通过与环境的交互进行学习,目前广泛应用于游戏领域,如ATARI游戏.西洋双陆棋.AlphaZero ...

  6. Spring系列(一):Spring的基本概念及其核心

    一.Spring是什么 Spring是一种多层的J2EE应用程序框架,其核心就是提供一种新的机制管理业务对象及其依赖关系. 二.为什么要使用Spring 1. 降低组件之间的耦合度,实现软件各层之间的 ...

  7. Kafka核心概念及核心机制

    核心概念 broker: 进程 producer: 生产者 flume consumer: 消费者 ss (spark streaming,,,) topic: 主题 分区+副本数 文件夹 parti ...

  8. Apollo(分布式配置中心)核心概念及核心功能介绍

    Apollo(阿波罗)是携程框架部门研发的分布式配置中心,能够集中化管理应用不同环境.不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限.流程治理等特性,适用于微服务配置管理场景. 服 ...

  9. gateway sentinel 熔断 不起作用_Sentinel 的一些概念与核心类介绍

    了解 Sentinel 的一些概念 资源:资源是 Sentinel 的关键概念.资源,可以是一个方法.一段代码.由应用提供的接口,或者由应用调用其它应用的接口. 规则:围绕资源的实时状态设定的规则,包 ...

最新文章

  1. mysql 8.0 docker_Docker安装MySQL8.0的实现方法
  2. 使用Leangoo管理产品Backlog
  3. 祝所有51cto的朋友光棍节快乐
  4. 自己盲目思考,不如看看经典方案
  5. windows7黑屏修复_如何在Windows 10更新后修复黑屏
  6. HttpMate简介
  7. c语言指针算法分析怎么写,什么叫指针算法啊??
  8. 查询商品列表报错This application has no explicit mapping for /error, so you are seeing this as a fallback
  9. 电商首焦素材的万能构图模板
  10. 指令汇B新闻客户端开发(四) 自动轮播条
  11. Edge浏览器,设定主页(默认打开页面)方法
  12. Python数据分析与机器学习项目实战
  13. 计算机职业资格证书如何在网上查询?
  14. 里程碑!家里的开发板能使用Dnspod的动态域名服务,稳定
  15. 实习僧招聘网站信息采集
  16. 简述sizeof和strlen的区别
  17. 企业邮箱使用安全注意事项
  18. 演讲者模式投影到幕布也看到备注_在PPT中插入备注时,如何在放映时只让演讲者看到,而观众看不到...
  19. 策略岗、分析岗、模型岗,在不同机构的组织架构是怎样的?
  20. 利用DHT网络原理制作bt采集

热门文章

  1. 7-21 输出大写英文字母 (15分)
  2. 将阿拉伯数字转换成汉字数字(小写)
  3. 《神魔之塔》高傲女巫的化身「恶龙之怒 地狱级」通关
  4. Java中set.iterator()的用法;
  5. (2) [保护模式]段描述符
  6. 学习PHP——高级(总)
  7. 必备干货,程序员的自我修养、高效学习方法
  8. 顺序图-循环和分支-操作符的使用
  9. 0x0报错解决--win11预览版升级报错0x0的快速解决方案
  10. 1077: 空心菱形