测试的必要性
当您在一个项目中工作时,您很可能不是唯一会使用这段代码的开发人员。即使你是唯一会修改代码的人,如果你在创建代码几周后才修改,你也很可能记不住这段代码受影响的所有地方。好吧,让我们假设你是唯一的开发人员,而且你的记忆力已经超出了极限;你能验证对一个经常使用的对象(如请求)所做的更改是否总能按预期运行吗?更重要的是,你愿意在每次进行微小更改时都这样做吗?
测试类型
在编写应用程序、修改现有代码或添加新功能时,获得良好的反馈非常重要。如何知道获得的反馈足够好呢?它应该符合 AEIOU 原则:
-
Automatic:获取反馈应尽可能简单。与手动测试应用程序相比,只需运行一条命令即可获得反馈。
-
Extensive:我们应该能够覆盖尽可能多的用例,包括编写代码时难以预见的边缘情况。
-
Immediate,即时性:应尽快得到反馈。这意味着,在引入变更后得到的反馈要比代码投入生产后得到的反馈好得多。
-
Open:测试结果应该是透明的,同时,测试也应该让其他开发人员了解如何集成或操作代码。
-
Useful:它应能回答诸如 "这一改动是否有效?"、"是否会意外破坏应用程序?"或 "是否存在无法正常工作的边缘情况?" 等问题。
因此,尽管一开始这个概念很奇怪,但测试代码的最好方法就是……用更多的代码。没错!我们编写代码的目的是测试应用程序的代码。为什么呢?因为这是我们所知道的能满足所有 AEIU 原则的最好方法,而且它还有以下优点:
-
我们只需在命令行或最喜欢的集成开发环境中运行一条命令即可执行测试。无需通过浏览器持续手动测试应用程序。
-
我们只需编写一次测试。一开始,这可能会有点痛苦,但一旦写好代码,就不需要反复重复了。也就是说,经过一段时间的努力,我们就能毫不费力地测试每一个案例。如果我们必须手动测试它以及所有用例和边缘用例,那将是一场噩梦。
-
要知道代码是否能正常工作,并不需要整个应用程序都能正常工作。想象一下,您正在编写路由器:为了知道它是否能正常工作,您必须等到应用程序在浏览器中正常运行时才能知道。相反,您可以编写测试,并在完成类后立即运行它们。
-
在编写测试时,您将获得有关失败原因的反馈。这对于了解路由器的特定功能何时失效以及失效原因非常有用,总比在浏览器上显示 500 错误要好。
希望我们现在已经让你明白,编写测试是必不可少的。不过,这是最容易的部分。问题是,我们知道有几种不同的方法。我们是编写测试整个应用程序的测试,还是编写测试特定部分的测试?我们是否要将测试区域与其他区域隔离?测试时要与数据库或其他外部资源交互吗?根据你的回答,你将决定要编写哪种类型的测试。让我们来讨论一下开发人员认同的三种主要方法:
-
单元测试:这些测试的范围非常集中。其目的是测试单个类或方法,将它们与其他代码隔离开来。以
Sale
域类为例:它有一些关于添加书籍的逻辑,对吗?单元测试可能只是实例化一个新的销售,将书籍添加到对象中,并验证书籍数组是否有效。由于单元测试的范围较小,因此速度超快,您可以轻松地对同一功能设置多个不同的场景,涵盖您能想象到的所有边缘情况。单元测试也是孤立的,这意味着我们不会太在意应用程序的所有部分是如何集成的。相反,我们将确保每个部分都能完美运行。 -
集成测试:这些测试的范围更广。其目的是验证应用程序的所有部分是否能协同工作,因此其范围不限于一个类或函数,而是包括一组类或整个应用程序。如果我们不想使用真正的数据库或依赖于其他外部网络服务,那么仍然需要进行一些隔离。我们应用程序中的一个例子是模拟一个请求对象,将其发送到路由器,并验证响应是否符合预期。
-
验收测试:这些测试的范围更广。它们试图从用户的角度测试整个功能。在网络应用程序中,这意味着我们可以启动浏览器,模拟用户的点击操作,每次都在浏览器中断言响应。是的,所有这一切都是通过代码实现的!可以想象,这些测试运行起来会比较慢,因为它们的范围比较大,而且使用浏览器也会大大降低运行速度。
那么,面对所有这些类型的测试,您应该编写哪一种呢?答案是全部。诀窍在于了解每种类型测试的时间和数量。一种好的方法是编写大量单元测试,涵盖代码中的所有内容;然后编写较少的集成测试,确保应用程序的所有组件都能协同工作;最后编写验收测试,但只测试应用程序的主要流程。下面的测试金字塔代表了这一理念:

原因很简单:真正的反馈将来自单元测试。因为执行单元测试既简单又快捷,所以当你写完修改后,单元测试就会告诉你是否有什么地方做错了。一旦知道所有类和函数的行为都符合预期,就需要验证它们是否能协同工作。不过,为此您不需要再次测试所有的边缘情况,因为在编写单元测试时您已经做了这些工作。在这里,您只需编写几个集成测试,以确认所有组件都能正常通信。最后,为了确保不仅代码能正常运行,而且用户体验也能达到预期目标,我们将编写验收测试,模拟用户浏览不同视图的过程。在这里,测试是非常缓慢的,只有在流程完成后才能进行测试,因此反馈会在稍后出现。我们将添加验收测试,以确保主要流程正常工作,但我们不需要测试每一个场景,因为我们已经通过集成测试和单元测试完成了这些工作。
单元测试和代码覆盖率
现在您已经知道什么是测试、为什么需要它们以及我们有哪些类型的测试,我们将在本章的其余部分重点讨论编写好的单元测试,因为它们将占用您大部分时间。
正如我们之前解释的,单元测试的想法是确保一段代码(通常是类或方法)按预期工作。由于方法包含的代码量应该很小,因此运行测试几乎不需要时间。利用这一点,我们将运行多个测试,尝试覆盖尽可能多的用例。
如果您不是第一次听说单元测试,那么您可能知道 代码覆盖率 的概念。这个概念指的是测试执行的代码量,也就是测试代码的百分比。例如,如果您的应用程序有 10,000 行代码,而您的测试总共测试了 7,500 行,那么您的代码覆盖率就是 75%。有一些工具可以在代码上显示标记,表明某一行是否经过测试,这对识别应用程序中哪些部分没有经过测试非常有用,从而提醒您更改这些部分会更危险。
然而,代码覆盖率是一把双刃剑。为什么会这样呢?这是因为开发人员往往痴迷于代码覆盖率,以 100% 的覆盖率为目标。但是,您应该知道,代码覆盖率只是结果,而不是您的目标。您的目标是编写单元测试,验证某些代码片段的所有用例,以便让您在每次更改代码时更有安全感。这就意味着,对于一个给定的方法,仅仅编写一个测试可能是不够的,因为同一行代码在不同的输入值下可能会有不同的表现。不过,如果你的重点是代码覆盖率,那么写一个测试就能满足你的要求,你可能就不需要再写任何测试了。