JavaScript 测试驱动开发简介
介绍
本文的目的是帮助您了解 JavaScript 中测试驱动开发 (TDD) 的基本概念。
我们将从以测试驱动的方式介绍一个小项目的开发开始。第一部分将重点介绍单元测试,最后一部分将重点介绍代码覆盖率。如果您想自己试验该项目,GitHub 上有一个存储库,其中汇集了本文中的所有代码。
等一下。什么是测试驱动开发?
简而言之,TDD 改变了我们的常规工作流程。传统上,软件开发工作流程主要是以下步骤的循环:
- 想想你的代码应该做什么。
- 编写代码来执行此操作。
- 测试代码以查看其是否有效。
例如,假设我们要编写一个函数add(number1, number2)来将两个数字相加。首先,我们编写add函数,然后尝试几个示例,看看它是否给出了我们期望的输出。我们可以尝试运行add(1,1)、add(5,7)和add(-4, 5),我们可能会得到输出2、12,然后……哎呀,一定是某个地方有错误,-9。
我们修改代码以尝试修复不正确的输出,然后再次运行add(-4, 5)。也许它会返回1,就像我们想要的那样。为了安全起见,我们将运行其他示例以查看它们是否仍提供正确的输出。哎呀,现在add(5,7)返回-2。我们将经历几个这样的循环:我们修改代码,然后尝试几个示例,直到我们足够确信我们的代码按我们想要的方式工作。
测试驱动开发通过编写自动化测试以及在编写代码之前编写测试来改变此工作流程。以下是我们的新工作流程:
- 想想你的代码应该做什么。
- 编写测试,指定您希望代码执行的操作。
- 编写代码来执行此操作。
- 通过运行您编写的测试来查看代码是否有效。
因此,在开始编写add函数之前,我们将编写一些测试代码来指定我们期望的输出。这称为单元测试。单元测试可能看起来像这样:
expect(add(1, 1)).toEqual(2);
expect(add(5, 7)).toEqual(12);
expect(add(-4, 5)).toEqual(1);
我们可以对任意数量的测试示例执行此操作。然后,我们将编写add函数。完成后,我们可以运行测试代码,它会告诉我们我们的函数是否通过了所有测试:
3 项测试中有 2 项通过。1 项测试失败:
预期add(-4,5)返回1,但得到-9。
好的,我们有一个测试失败了。我们将修改代码以尝试纠正错误,然后再次运行测试:
3 项测试中有 1 项通过。2 项测试失败:
预期add(1,1)返回2,但得到0。
预期add(5,7)返回12,但得到-2。
糟糕,我们修复了一件事,但同时破坏了其他事情。我们将再次修改代码,看看是否还有问题:
3 项测试已通过 3 项。好极了!
当然,仅仅因为我们的代码通过了测试并不意味着代码总体上是有效的。但它确实让我们对其正确性更有信心。如果我们将来发现测试遗漏的错误,我们总是可以添加更多测试以获得更好的覆盖范围。
你们中许多人可能会反对,“但是那有什么意义呢?那不是只会带来很多无意义的额外麻烦吗? ”
确实,设置测试环境和弄清楚如何编写单元测试通常需要一些努力。从短期来看,按照传统方式做事更快。但从长远来看,TDD 可以节省时间,否则会浪费在重复手动测试同一件事上。而且单元测试还有许多其他好处:
自动回归检测
有时,您会在程序中编写错误,导致以前正常运行的代码不再正常运行,或者您会意外地重新引入以前修复的旧错误。这称为回归。如果您没有任何自动测试,回归可能会在很长一段时间内被忽视。通过单元测试并不能保证您的代码正常运行,但如果您为修复的每个错误编写测试,通过单元测试可以保证的一件事是您没有重新引入旧错误。
大胆重构
代码很快就会变得混乱,但重构代码往往令人生畏,因为很有可能在重构过程中破坏某些功能。毕竟,代码看起来往往很混乱,是因为你必须拼凑一些变通方法才能使其适用于罕见的边缘情况。当你尝试清理代码,甚至从头开始重写代码时,它很可能会在这些边缘情况下失败。如果你有涵盖这些边缘情况的单元测试,你会立即发现你破坏了某些功能,并且可以更勇敢地进行更改。
文档
如果另一个开发人员(或者可能是未来的你)无法弄清楚如何使用你编写的代码,他们可以查看单元测试,了解代码的设计用途。当然,单元测试不能替代真正的文档,但肯定比没有文档要好(这种情况很常见,因为程序员的优先级几乎总是高于编写文档)。
鲁棒性
当您没有自动化测试并且应用程序变得足够复杂时,代码很容易让人感觉非常脆弱。也就是说,当您使用它时它似乎工作正常(大多数时候),但您有一种挥之不去的担忧,担心用户最轻微的意外操作或未来对代码的最轻微修改都会导致一切崩溃。知道您的代码通过了一套单元测试比知道您的代码在您前几天用一些示例手动测试时似乎可以正常工作更令人放心。
好的,理论讲得够多了,让我们开始实际操作,看看这在实践中是如何运作的。
实例
在这个例子中,我们将以测试驱动的方式介绍开发简单日期库的过程。对于库的每个部分,我们将首先编写测试来指定我们希望它如何表现,然后编写代码来实现该行为。如果测试失败,我们就知道实现不符合规范。请记住,此代码的目的仅用于演示测试驱动开发,而不是用于实际使用的功能齐全的日期库。如果您在寻找日期库时偶然发现了这篇文章,我推荐Moment.js。
设置测试环境
我们要做的第一件事是安装一个测试库。QUnit 、Mocha和Jasmine目前最受欢迎(使用哪一个并不重要,因为它们本质上都做同样的事情)。在本文中,我随意选择了 Jasmine 。
以下是我的设置方法:
- 为该项目创建一个文件夹,并有一个名为test 的子文件夹。
- 下载Jasmine 独立 zip 文件(我使用的是 2.4.1 版本)并将其解压到您的测试文件夹中。
- 如果你打开SpecRunner.html文件,你应该会看到类似这样的内容:
- 在父文件夹中,创建一个名为DateTime.js的文件,并在test/spec文件夹中创建一个名为DateTimeSpec.js的文件。删除spec文件夹中的其他文件;我们不再需要它们了。
- 编辑SpecRunner.html文件的头部来引用我们的脚本。它看起来应该像这样:
<script src="lib/jasmine-2.4.1/jasmine.js"></script>
<script src="lib/jasmine-2.4.1/jasmine-html.js"></script>
<script src="lib/jasmine-2.4.1/boot.js"></script>
<!-- include source files here... -->
<script src="../DateTime.js" data-cover></script>
<!-- include spec files here... -->
<script src="spec/DateTimeSpec.js"></script>
如果您现在刷新SpecRunner.html,它应该会显示“未找到规格”,因为我们尚未写入任何内容。完成这些后,我们现在可以开始构建库了。
DateTime.js API
您可以快速浏览本节,以基本了解我们的日期库将做什么。无需记住这里的每一个细节;如果您对代码的预期行为感到困惑,您可以随时参考本节。
DateTime是一个以下列方式之一构造日期的函数:
DateTime()不带参数调用,创建一个表示当前日期/时间的对象。
var d = DateTime(); // d represents the current date/time.
DateTime(date), called with one argument date, a native JavaScript Date object, creates an object representing the date/time corresponding to date.
var d = DateTime(new Date(0)); // d represents 1 Jan 1970 00:00:00 GMT
DateTime(dateString, formatString), called with two arguments dateString and formatString, returns an object representing the date/time encoded in dateString, which is interpreted using the format specified in formatString.
var d = DateTime("1/5/2012", "D/M/YYYY");
The object returned by DateTime will have the following method
toString(formatString?) - returns a string representation of the date, using the optional formatString argument to specify how the output should be formatted. If no formatString is provided, it will default to "YYYY-M-D H:m:s".
> DateTime().toString() "2016-2-22 15:06:42" > DateTime().toString("MMMM Do, YYYY") "February 22nd, 2016"
And the following properties:
- year - the full (usually 4-digit) year (e.g. 1997). Represented in a format string as YYYY. Readable/writable.
- monthName - the name of the month (e.g. December). Represented in a format string as MMMM. Readable/writable.
- month - the number of the month (e.g. 12 for December). Represented in a format string as M. Readable/writable.
- day - the name of the weekday (e.g. Tuesday). Represented in a format string as dddd. Readonly.
- date - the date of the month (e.g. 22). Represented in a format string as D. Readable/writable.
- ordinalDate - the ordinal date of the month (e.g. 22nd). Represented in a format string as Do. Readable/writable.
- hours - a number from 0 to 23, corresponding to the hour. Represented in a format string as H. Readable/writable.
- hours12 - a number from 1 to 12, corresponding to the hour in the 12-hour system. Represented in a format string as h. Readable/writable.
- minutes - a number from 0 to 59, corresponding to the minute. Represented in a format string as m. Readable/writable.
- seconds - a number from 0 to 59, corresponding to the minute. Represented in a format string as s. Readable/writable.
- ampm - a string, either "am" or "pm". Represented in a format string as a. Readable/writable.
- offset - returns the Unix offset, that is, the number of milliseconds since January 1st, 1970, 00:00:00. Readable/writable.
入门
我们要做的第一件事是确定要实现的 API 的最小子集,然后在此基础上逐步构建,直到完成。一个合理的起点是创建一个DateTime构造函数,该构造函数返回一个表示当前时间的对象。考虑到这一点,我们将编写一个单元测试,指定我们期望DateTime<font style="vertic
免责声明:本内容来源于第三方作者授权、网友推荐或互联网整理,旨在为广大用户提供学习与参考之用。所有文本和图片版权归原创网站或作者本人所有,其观点并不代表本站立场。如有任何版权侵犯或转载不当之情况,请与我们取得联系,我们将尽快进行相关处理与修改。感谢您的理解与支持!
请先 登录后发表评论 ~