QCon全球软件开发大会8折优惠倒计时,购票立减¥1760!了解详情 >>> 了解详情
写点什么

迈出单元测试的第一步

2012 年 7 月 24 日

单元测试不仅是软件行业的最佳实践,在敏捷方法的推动下,它也成为了可持续软件生产的支柱。根据最新的年度敏捷调查,70% 的参与者会对他们的代码进行单元测试。

单元测试和其他敏捷实践密切相关,所以开始编写测试是组织向敏捷转型的踏脚石。道路漫长,但值得去做。我将在本文介绍符合要求的小技巧,以及在开发周期里进行单元测试的步骤。

有效的单元测试默认要能自动化。没有自动化,生产力就会下降。没有自动化,单元测试的习惯也不会持续太久。依靠手工测试(由测试人员或开发人员完成)并不能持续太长时间;在有压力的情况下,没人会记得去运行所有的测试,或者去覆盖所有的场景。自动化是我们的朋友,所有的单元测试框架都支持自动化,而且集成了其他自动化系统。

单元测试对现代开发来说至关重要

有代码相关的测试,我们就有一个天然的安全保障。我们修改的代码要是带来了什么问题,测试会告诉我们。这个安全保障越健全,我们对代码正常运行的信心就越大,对按需修改代码的能力也就越有信心。

和其他类型的测试相比,单元测试的主要优点是反馈迅速。在几秒钟内运行数百个成套的测试,这对开发流程很有帮助。我们会形成“添加一些代码,添加测试,测试运行通过,前进”的节奏。小步前进、确保一切正常也意味着调试时间会大大减少。测试能提高生产力也就不足为奇了——在Bug 上少花时间,把更多的时间用到新功能的推出上。

依赖关系的壁垒

给新建项目添加测试相当容易——毕竟代码不会阻碍测试。不过这种情况绝对不常见。大多数人都是在处理遗留代码,这些代码不太容易测试,有时候甚至运行不起来——它需要的数据或配置可能只存在于生产服务器上。我们或许要为不同的场景创建不同的设置,这也许会花费过多的精力。在很多情况下,我们可能还会为了测试修改代码。这让人无法理解:我们编写测试就是为了能有修改代码的信心,还没有测试又该如何去稳妥地修改代码呢?

代码可测性是语言和工具的功能。大家认为Ruby 等动态语言是可测的。对于测试的内部代码,我们可以改变其依赖关系的行为,而不用修改生产代码。C#或Java 等静态类型语言则不太容易去测试。

下面有个例子:一个C#的过期检查方法,检查是否超过了特定日期:

复制代码
<span color="#0000ff">public class</span> <span color="#4bacc6">ExpirationChecker</span>
{
    <span color="#0000ff">private readonly</span> <span color="#4bacc6">DateTime</span> expirationDate = <span color="#0000ff">new</span> <span color="#4bacc6">DateTime</span>(2012, 1, 1);
    <span color="#0000ff">public bool</span> IsExpired()
    {
       <span color="#0000ff"> if</span> (<span color="#4bacc6">DateTime</span>.Now > expirationDate)
        {
            <span color="#0000ff">return true</span>;
        }
        <span color="#0000ff">return false</span>;
    }
}

在这个例子里,IsExpired 方法的 DateTime 属性对测试运行时间有强依赖。Now 返回的是实际时间。这个方法有两种情况,它会根据日期返回不同的值。修改计算机时间是绝对不行的,因为我们要在任何时候到任何计算机上去测试场景,并且不能带来任何副作用。

要测试到两种情况,一种可能的解决方案是修改代码。比如说,我们可以把代码修改成:

复制代码
<span color="#0000ff">public bool</span> IsExpired(<span color="#4bacc6">DateTime</span> now)
{
   <span color="#0000ff"> if</span> (now > expirationDate)
    {
        <span color="#0000ff">return true</span>;
    }
    <span color="#0000ff">return false</span>;
}

这样,测试可以注入不同、可控的 DateTime 值,而不用在生产代码里写定一个值。我们要是不能修改代码,可以利用 Typemock Isolator 等 Mocking 框架,模拟静态属性和方法。针对先前的代码,测试可以写成:

复制代码
[<span color="#4bacc6">TestMethod</span>]
<span color="#0000ff">public void</span> IsExpired_BeforeExpirationDate_ReturnFalse()
{
   <span color="#4bacc6"> Isolate</span>.WhenCalled(() => <span color="#4bacc6">DateTime</span>.Now)
        .WillReturn(<span color="#0000ff">new</span> <span color="#4bacc6">DateTime</span>(2000, 1, 1));
    <span color="#4bacc6">ExpirationChecker</span> checker = <span color="#0000ff">new</span> <span color="#4bacc6">ExpirationChecker</span>();
    <span color="#0000ff">var</span> result = checker.IsExpired();
    <span color="#4bacc6">Assert</span>.IsFalse(result);
}

现有的遗留代码不能轻易修改,因为我们没有针对它的测试。开始测试遗留代码之后,我们就能明白:代码越丑陋,测试越困难。工具可以减轻一些痛苦,但我们要努力去构建安全的环境。

依赖关系并不是唯一的内容……

我们很快会遇到的另一个问题是测试维护:测试和被测试代码耦合在一起。有耦合关系,修改生产代码就有可能破坏测试。要是代码修改引起测试失败,我们就需要回去解决这些问题。很多开发人员害怕维护两个代码库,这种恐惧甚至会让他们干脆不进行单元测试。真正的维护工作既取决于工具,也取决于技巧。

编写好的测试是通过实践获得的技能。编写的测试越多,我们就越精于此,同时会提升测试质量,维护也越来越少。有了测试,我们就有机会重构代码,这反过来又会让测试更简洁、更易读、更健壮。

工具对实践的难易程度有极大的影响。在基础层,我们需要一个测试框架和一个 Mocking 框架。在.Net 领域,两种框架的选择都很丰富。

编写第一个测试的准则

开始的时候,我们通常会试用不同的工具,来理解他们的工作原理。我们往往不会在实际的工作代码上开始编写测试。但很快就要给代码编写真正的测试。有一些小提示届时会有用:

  • 从哪里开始:一般来说,我们编写测试是针对工作代码的,无论代码是 Bug 修复还是新功能。对 Bug 修复来说,编写的测试要检查修复。对功能来说,测试应检查正确的行为。

  • 支架:以我们掌握的知识来看,明智的做法是先添加能确保当前实现运行的测试。添加新的代码之前先写测试,因为我们希望在修改现有代码之前,能有安全的保障。这些测试被称为“特征测试”,这个术语来自 Michael Feathers 编写的《修改代码的艺术》。

  • 命名:测试最重要的属性是它的名字。我们一般不会去看运行通过的测试。但当它失败时,我们看的就是它的名字。所以挑一个好名字,描述出场景和代码的预期结果。好名字还有助于我们定位测试里的 Bug。

  • 评审:为了增加测试成功通过的机会,编写第一个测试时我们应该和同事结对。两个人都能从实践中学习,而且我们还能立即评审测试。最好对所测的内容、测试的名称达成共识,因为这会成为团队其他人员的基本模板。

  • AAA:现代测试的结构符合 AAA 模式——Arrange(测试设置)、Act(调用测试里的代码)、Assert(测试通过的标准)。如果我们使用测试驱动开发(TDD),我们要先编写完整的测试,然后再添加代码。对遗留代码来说,我们可能需要换一种方式。一旦我们有一个场景和名称需要测试,那先编写 Act 和 Assert 部分。我们要不停构建 Arrange 部分,因为对需要准备或仿造的依赖关系,我们知道的要更多一些。然后继续这么做,直到有一个测试能够通过。

  • 重构:一旦准备好了测试,我们就可以重构代码了。重构和测试都是后天获得的技能。我们不仅要重构被测试代码,也要重构测试本身。但 DRY(不要重复自己)原则不适用于测试。测试失败时,我们希望尽快修复问题,所有的测试代码最好在一个地方,而不是分散在不同的文件里。

  • 可读性:测试应该是可读的,最好是人类可读。和搭档评审测试代码,看他能否理解测试的目的。评审其他测试,看看它们的名称和内容怎样与相邻的测试区分开来。一旦测试失败,就需要修复它们,最好还是在运行失败之前评审它们。

  • 组织:一旦我们有了更多的测试,组织就有了用武之地。测试可以在很多方面有所不同,但最明显的一个就是如何快速运行。有些测试可能在毫秒内运行完,而有些则需要数秒或好几分钟。和工作一样,我们都希望得到最快的反馈。这就是前面谈到的怎么按一定的节奏去进行。要做到这一点,你应该把测试划分一下,把快的测试和慢的测试分开运行。这能手工(努力)去做,但在.NET 领域,Typemock Isolator 有一个运行器,能自动按运行速度分离。

总结

迈出单元测试的第一步是很有挑战的。体验依赖的东西很多——语言、工具、现有代码、依赖关系和技能。只要稍稍思考,进行大量训练和实践,你就能渐入测试的佳境。

作者简介

Gil Zilberfeld Typemock 的产品经理。Gil 有着十五年多的软件开发经验,从事过软件开发各个方面的工作,从编码到团队管理和流程实施。Gil 的很多博客文章( www.gilzilberfeld.com )和演讲都是关于单元测试的,他鼓励开发人员(无论是初学者还是经验丰富的开发人员)能在项目里实践单元测试。你可以通过 gilz@typemock.com 联系到他。

查看英文原文 First Steps in Unit Testing


给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。

2012 年 7 月 24 日 00:004125
用户头像

发布了 151 篇内容, 共 53.8 次阅读, 收获喜欢 16 次。

关注

评论

发布
暂无评论
发现更多内容

Mybatis【3】-- Mybatis使用工具类读取配置文件以及从属性读取DB信息

秦怀杂货店

Java 数据库 mybatis

10.4领域驱动设计DDD

张荣召

【Java基础】-- isAssignableFrom的用法详细解析

秦怀杂货店

Java 关键字

架构师训练营 1 期第 10 周:模块分解 - 作业

piercebn

极客大学架构师训练营

架构师训练营第10周课后练习

薛凯

架构师训练营第 10 周作业

netspecial

极客大学架构师训练营

10.1微服务:服务本身的设计,维护及治理

张荣召

学习总结--week10

张荣召

接口测试--接口文档规范

测试人生路

接口文档

最佳的思维导图生成工具——markmap 使用教程

白色蜗牛

Java 程序员 职场 实用工具

今日份学习之Spring Boot自动配置实现原理

比伯

Java 编程 架构 面试 计算机

Mybatis【4】-- 关于Mybatis别名定义

秦怀杂货店

Java mybatis

架构师训练营-week10

睁眼看世界

极客大学架构师训练营

一次有效的产品需求头脑风暴

Bruce Talk

敏捷开发 Agile Product Owner

Mybatis【2.3】-- Mybatis一定要使用commit才能成功修改数据么?

秦怀杂货店

Java 数据库 mybatis

10.5软件组件设计原则

张荣召

架构师Week6作业

lggl

作业

码了2000多行代码就是为了讲清楚TLS握手流程

Gopher指北

golang https 后端

STL 源码剖析之五大组态常量介绍

herongwei

c++ 源码 后端 stl

redis 基础数据 sets 业务场景分析

sinsy

redis 业务场景分析

10.2微服务:落地实践的策略与思路

张荣召

架构师训练营第十周学习总结

Gosling

极客大学架构师训练营

第十周总结

睁眼看世界

极客大学架构师训练营

架构师Week6总结

lggl

总结

Mybatis【2.2】-- Mybatis关于创建SqlSession源码分析的几点疑问?

秦怀杂货店

Java 数据库 mybatis

10.7作业

张荣召

10.3微服务网关的技术架构

张荣召

JDBC【4】-- SPI底层原理解析

秦怀杂货店

Java 源码 spi

食堂就餐卡系统 UML 设计

心晴雨亦晴(~o~)

极客大学架构师训练营

内推阿里,朋友说让我学会这46道面试题,我不信,现在我后悔了

小Q

Java 学习 编程 架构 面试

一万三千字的HashMap面试必问知识点详解

云流

Java 编程 面试 计算机

移动应用开发的下一站

移动应用开发的下一站

迈出单元测试的第一步-InfoQ