文档结构  
翻译进度:已翻译     翻译赏金:0 元 (?)    ¥ 我要打赏

对于测试驱动开发和单元测试,我总有一种既爱又恨的感情在里面。

我一直都是这些“最佳实践”的热心支持者,但是我对它们的使用也一直持有怀疑态度。

软件开发中最大的问题之一是开发者或管理者仅仅是因为它们是最佳实践才要去应用这些“最佳实践” ,却不去理解它们的原因或真正的用途

我记得曾经工作在一个这样的软件项目中,据说我们要修改的软件里有一批惊人数量的单元测试––大概有三千左右。

第 1 段(可获 1.26 积分)

通常来说,这是一个好迹象。

这可能意味着该项目的开发者同时也在践行其他的最佳实践,并且在代码层面的架构做了一些表面上的工作。

我当时听到这个消息是非常高兴的,因为这样会使得我作为一个开发团队的导师或教练的工作会相当轻松了。既然我们已经有了单元测试,那么我要做的就只是让新的团队继续保持他们的风格并完成他们自己的单元测试。

我打开我的IDE,然后将项目加载进来。

那是一个相当大的项目。

我看到一个文件夹名字是“(unit tests)单元测试”。

第 2 段(可获 1.4 积分)

很好。运行它们看看会发生些什么。

只用了几分钟的时间,而且出乎我的意料之外的是所有的测试都执行了,而且所有的测试结果都显示为绿色。他们全部都通过了。

现在我就有点怀疑了。三千多个单元测试,他们都通过了吗?

到底是怎么回事?

当我刚进入一个新项目去做教练时,大部分时间如果没有任何单元测试的话,会有一大堆测试都会失败。

于是我决定随机抽查一个测试瞅瞅。

乍一看,看起来相当的合情合理。

并不是最好的测试,大部分是我曾见过的解释型的测试,但是我还是可以弄清楚它在做什么。

第 3 段(可获 1.46 积分)

但之后我注意到了一些事情…

并没有断言。

也就是说根本没有东西被测试

这个测试分了几步而且这些步骤都在运行,但是在测试的结尾应该检查些什么东西的,然而并没有检查。

这个所谓的“测试” 其实并没有在测试任何东西。

然后我打开了另外的测试。

更糟糕了。

那个能够在某种程度上测试一些东西的断言语句却被注释掉了。

哇,那确实是让一个测试通过的不错的方法,仅仅将使它失败的代码注释掉就好了。

我接着去检查了各个测试。

没有一个能够测试什么东西。

三千个测试全部都是毫无用处的。

第 4 段(可获 1.33 积分)

写单元测试跟理解单元测试,以及测试驱动开发是有很大不同的。

什么是单元测试?

单元测试的基本思想是编写可以执行最小“单元”代码的测试。

单元测试通常跟要测的源代码使用同一种编程语言,并且会直接使用到源代码。

可以将单元测试看作是测试其他代码的代码。

当我使用“测试”这个字眼的时候,我是指相当宽松的含义,因为单元测试实际上不是测试。他们并不能测试任何东西。

第 5 段(可获 1.23 积分)

我的意思是,当你运行单元测试时,你通常不会发现有些代码不工作。

只有当你写 单元测试时,才会发现那些不工作的代码。

是的,代码可能会更改,测试可能失败,因此在这个意义上,单元测试是一个回归测试。 然而,一般来说,单元测试不像常规测试那样,需要执行一些步骤,并且看看软件是否正确运行。

作为编写单元测试的开发人员,你可以在编写单元测试时发现代码是否正常工作,因为你将持续修改代码,直到单元测试通过。

第 6 段(可获 1.69 积分)

为什么要写一个单元测试,而不是确保单元测试通过?

当你以这种方式思考时,单元测试更多地是在一个非常低的水平上指定特定的代码单元的绝对需求。

您可以将单元测试视为绝对规范。

单元测试指定在这些条件下使用这个特定的输入集合,这是我应该从这个代码单元获得的输出。

真正的单元测试可以测试代码的最小内聚单元,在大多数编程语言中 - 至少是面向对象的编程语言中 - 是一个类。

第 7 段(可获 1.28 积分)

单元测试有时叫什么?

通常,单元测试与集成测试混淆。

一些“单元测试”测试多个类或测试更大的代码单元。

大量的开发人员会认为这些仍然是单元测试,因为他们是在低层次的代码中写的白盒测试。

你不应该与这些人争论。

只要知道这些是真正的集成测试就行了,真正的单元测试测试那些可能孤立的最小的代码单元。

另一件通常被称为单元测试的东西 - 但实际上并不是什么 - 是编写没有断言的单元测试。 换句话说,单元测试没有实际测试任何东西。

第 8 段(可获 1.46 积分)

任何测试,单元测试或不是单元测试,都应该有某种检查 - 我们称之为断言 - 在最后决定它是通过还是失败。

总是通过的测试是无用的。

总是失败的测试是无用的。

单元测试的价值

为什么我对单元测试这样坚持?

调用单元测试实际测试或不测试孤立的最小单元有什么危害?

那么,如果我的一些测试没有断言怎么办? 他们至少在执行代码。

好吧,让我试着解释。

执行单元测试有两个主要好处或原因。

第 9 段(可获 1.36 积分)

第一个是改进代码的设计

还记住我说过单元测试实际上不是测试吗?

当你编写正确的单元测试,强迫你孤立最小的代码单元时,你会发现在代码的设计中的问题

你可能会发现隔离类非常困难,不包括它的依赖,这可能使你意识到你的代码太紧耦合。

你可能会发现,你尝试测试的基本功能分布在多个单元中,这可能会使你意识到你的代码还不够紧密。

第 10 段(可获 1.33 积分)

你可能会发现当你坐下来写一个单元测试时,你意识到你不知道代码应该做什么,所以你不能为它写单元测试,相信我,这种情况经常发生。

当然,因为单元测试迫使你考虑一些边缘情况或测试你可能没有考虑的多个输入,在代码的实现中你可能会发现一个实际的错误。

通过编写单元测试,并严格遵守让它们独立测试最小的代码单元,你会发现该代码和这些单元测试设计的各种问题。

第 11 段(可获 1.4 积分)

在软件开发生命周期中,单元测试更多的是评估活动,而不是测试活动。

单元测试的第二个主要目的是创建一个自动化的回归测试集,它可以作为软件低层次的行为规范。

这意味着什么?

当你修改了代码,不会引入新的错误。

那种方式的单元测试是:回归测试。

但单元测试的目的不是仅仅建立这些回归测试。

在现实世界中,很少有回归被单元测试捕获,因为改变你测试的代码单元几乎总是涉及改变单元测试本身。

第 12 段(可获 1.4 积分)

回归测试在更高层次上作为黑盒测试活动更有效,因为在该级别,代码的内部结构可以改变,而外部行为预期保持相同。

单元测试测试内部结构,所以当结构改变时,单元测试不会“失败”。它们变得无效,必须改变,抛出或重写。

现在你比大多数10年的软件开发老手更了解单元测试的真正目的。

什么是测试驱动开发(TDD)?

还记得我们谈论软件开发方法的章节吗,里面提到瀑布方法通常没有实际工作,因为我们从来没有完整的规格。

第 13 段(可获 1.46 积分)

TDD的想法是,在你写任何代码之前,你先写一个测试作为一种规范,规定代码应该做什么。

这是一个非常强大的软件开发概念,但常常被滥用。

TDD通常意味着使用单元测试来驱动编写的产品代码,但它可以在任何级别应用。

然而,为了本章的目的,我们将坚持最常见的单元测试:应用程序。

TDD翻转事物,所以不是首先编写代码,然后编写单元测试来测试该代码(我们知道情况并非如此),你将首先编写单元测试,然后编写足够的代码使测试通过。

第 14 段(可获 1.66 积分)

通过这种方式,单元测试就“驱动”了代码的开发。

这一过程反复重复。

你编写另一个测试,该测试定义代码应该实现的更多的功能。

你更改代码或添加代码以使测试通过。

最后,你重构代码或清理,以使它更简洁。

这通常被称为“红,绿,重构”,因为首先单元测试失败(红色),然后编写代码使它通过(绿色),最后代码被重构。

TDD的目的是什么?

就像单元测试本身可以是一个错误使用的最佳实践,TDD也可以。

第 15 段(可获 1.45 积分)

很容易调用你正在做的TDD,甚至遵循实践,但是你或许不明白为什么这样做或它提供的价值(如果有的话)。

TDD的最大价值是测试产生优秀的规范。

TDD本质上是编写明确规范的实践,它可以在编写代码之前进行自动检查。

为什么测试是如此伟大的规范?

因为他们从不撒谎。

他们不会告诉你你的代码应该这样工作,然后在你花了两周时间喝着威士并使一切准备好后告诉你,它实际应该以另一种方式工作并说“这是错的; 这根本就不是我说的”。

第 16 段(可获 1.5 积分)

如果正确编写测试,它要么通过要么失败。

测试明确指出在某种情况下应该发生什么情况。

因此,在这方面,我们可以说TDD的目的是确保我们在实施之前充分了解我们正在实施的内容,并确保我们“正确”。

如果你坐下来做TDD,但你不知道测试应该测试什么,这意味着你需要去问更多的问题。

TDD的另一个价值是保持代码精益和简洁。

代码维护成本很高。

我经常开玩笑说,最好的程序员是那些写最少的代码,甚至找到方法删除代码的人,因为程序员已经找到一个确定的方式来减少错误并降低应用程序的维护成本。

第 17 段(可获 1.78 积分)

通过使用TDD,你可以非常确定你不用编写那些不必要的代码,因为你只会编写使测试通过的代码。

在软件开发中有一个称为YAGNI的原则,也称为你不需要它(you ain’t going to need it)。

TDD阻碍YAGNI。

典型的TDD工作流

从纯粹的学术角度来理解TDD有点困难,所以让我们来探讨一个示例TDD会话的流程。

你坐在你的办公桌前,快速勾画出你认为将是一个允许用户登录到应用程序的高级功能设计,并在他们忘记密码时更改密码。

第 18 段(可获 1.48 积分)

你决定首先通过创建一个类来实现登录功能,该类将处理登录过程的所有逻辑。

你打开你最喜欢的编辑器,并创建一个单元测试,称为“登录信息为空不能登录”。

你编写单元测试代码,首先创建一个Login类(你尚未创建)的实例。

然后,你编写一些代码以调用Login类的方法,该方法传递一个空的用户名和密码。

最后,你创建一个断言或断言(assert),断言用户确实没有登录。

第 19 段(可获 1.38 积分)

你试图运行测试,但它甚至不能编译,因为你没有一个Login类。

你可以通过创建Login类、用于登录的该类的方法和用于检查用户的状态以查看他们是否登录的其他方法来修复这种情况。

你将此类和方法中的功能全部留空。

你运行测试,这次它可以编译,但很快失败。

现在,你返回代码并实现足够的功能,以使测试通过。

在这种情况下,这意味着总是返回用户未登录。

第 20 段(可获 1.34 积分)

你再次运行测试,现在它通过了。

进入下一个测试。

这次你决定写一个名为“当用户有有效的用户名和密码时用户登录”的测试。

你编写一个单元测试,创建一个Login类的实例,并尝试使用用户名和密码登录。

在单元测试中,你写一个断言,Login类应该返回用户已经登录。

你运行这个新的测试,因为你的Login类总是返回用户没有登录,它当然会失败。

你回到你的Login类并实现一些代码来检查用户是否登录。

第 21 段(可获 1.46 积分)

在这种情况下,你必须想办法如何让这个单元测试保持孤立。

现在,完成使这项工作最简单的方法是硬编码你在测试中使用的用户名和密码,如果它匹配,那么你将返回用户已登录。

你更改代码,运行这两个测试,他们都通过了。

现在你看看你创建的代码,看看是否有一种方法可以重构它,可以使它更简单。

所以,你去创建更多的测试,编写足够的代码以使它们通过,然后重构你写的代码,直到为实现你的功能没有更多的、你能想到的测试用例为止。

第 22 段(可获 1.59 积分)

这些只是基础


这就是单元测试和测试驱动开发。

这些是TDD和单元测试的基础——但它们只是基础

当你真正尝试和隔离代码单元时,TDD可能会变得更复杂,因为代码是连接在一起的。

很少的类是完全隔离的。

相反,它们具有依赖性,而这些依赖性也有依赖性等等。

为了处理像这样的情况,经验丰富的TDDers使用mocks,这可以帮助你通过使用预设值mock依赖的功能来隔离个别类。

由于这是TDD和单元测试的基本概述,我们不会在这里详细讨论mock和其他TDD技术,但是请注意,本章中介绍的内容是一个简化的视图。

我们的想法是给你TDD和单元测试的基本概念和原则,希望你现在拥有了。

第 23 段(可获 1.94 积分)

文章评论

访客
讲的不错,很多开发人员都很不重视单元测试