所属分类:web前端开发
通过确保您的应用程序经过测试,您可以减少代码中发现的错误数量,提高应用程序的可维护性,并设计结构良好的代码。
客户端单元测试提出了与服务器端测试不同的挑战。在处理客户端代码时,您会发现自己很难将应用程序逻辑与 DOM 逻辑分开,并且通常只是构建 JavaScript 代码。幸运的是,有很多很棒的客户端测试库可以帮助测试您的代码、创建有关测试覆盖率的指标以及分析其复杂性。
首先,单元测试通常是一种通过确保应用程序按预期运行来减少错误的方法。除此之外,还有测试驱动开发 (TDD) 和行为驱动开发 (BDD) 的概念。
这两种单元测试策略将帮助您在编写应用程序逻辑之前编写测试来设计应用程序。通过在编写代码之前编写测试,您将有机会仔细思考应用程序的设计。
发生这种情况是因为当您编写测试时,您基本上是在尝试设计与代码交互的 API,因此您可以更好地了解其设计。首先进行测试将很快显示出设计中存在的任何缺陷,因为您正在编写的测试代码本质上使用了您正在编写的代码!
TDD 是一个代码发现过程
您将了解到 TDD 可以帮助您在编写代码时发现代码。 TDD 很快就可以概括为“红、绿、重构”。这意味着,您编写一个测试,编写足够的代码首先使测试失败。 然后,您编写使测试通过的代码。之后,你仔细思考你刚刚写的内容并重构它。又好又简单。
BDD 与 TDD 略有不同,更多地基于业务需求和规范。
您应该测试客户端代码的原因有很多。如前所述,它将有助于减少错误,并帮助您设计应用程序。客户端测试也很重要,因为它使您有机会独立测试前端代码,远离演示文稿。换句话说,它的优点之一是您可以测试 JavaScript 代码,而无需实际启动应用程序服务器。您只需运行测试并确保功能正常运行,而无需四处点击并进行测试。在许多情况下,只要正确设置测试,您甚至不需要访问互联网。
随着 JavaScript 在现代 Web 开发中发挥如此重要的作用,学习如何测试代码并减少错误进入生产代码的机会非常重要。您的老板不喜欢这种情况发生,您也不应该!事实上,开始进行客户端测试的一个好地方是围绕错误报告编写测试。当您没有地方从头开始时,这将使您能够练习编写测试。
测试客户端代码的另一个原因是,一旦存在一套测试并且对您的代码有适当的覆盖,当您准备好向代码添加新功能时,您将能够添加新功能功能,重新运行您的测试,并确保您没有退化或破坏任何现有功能。
如果您以前从未做过客户端测试,那么开始进行客户端测试可能会令人畏惧。客户端测试最困难的部分之一是找出将 DOM 与应用程序逻辑隔离的最佳方法。这通常意味着您需要对 DOM 进行某种抽象。实现这一目标的最简单方法是通过客户端框架,例如 Knockout.js、Backbone.js 或 Angular.js,仅举几例。
使用此类库时,您可以少考虑页面在浏览器中的呈现方式,而多考虑应用程序的功能。不过,用简单的 JavaScript 进行单元测试并不是不可能的。在这种情况下,如果您以 DOM 可以轻松抽象的方式设计代码,您的生活将会轻松得多。
有很多不同的测试库可供选择,尽管三个领先者往往是 QUnit、Mocha 和 Jasmine。
Jasmine 和 Mocha 都来自 BDD 单元测试学派,而 QUnit 只是它自己的一个单元测试框架。
在本文的其余部分中,我们将探讨使用 QUnit,因为它进入客户端测试的门槛非常低。查看 QUnit 的详细介绍以获取更多信息。
QUnit 入门非常简单。您只需要以下 HTML:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>QUnit Example</title> <link rel="stylesheet" href="qunit.css"> </head> <body> <div id="qunit"></div> <div id="qunit-fixture"></div> <script src="qunit.js"></script> <script src="../app/yourSourceCode.js"></script> <script src="tests.js"></script> </body> </html>
对于接下来的几个示例,假设我们正在构建一个小部件,您可以在文本框中输入邮政编码,然后它会使用 Geonames 返回相应的城市、州和县值。它一开始只显示邮政编码,但一旦邮政编码有五个字符,它就会从 Geonames 检索数据。如果能够找到数据,它将显示更多包含结果城市、州和县信息的字段。我们还将使用 Knockout.js。第一步是编写失败的测试。
在编写第一个测试之前稍微思考一下设计,可能需要至少两个 viewModel,所以这将是一个很好的起点。首先,我们将定义一个 QUnit 模块和我们的第一个测试:
module("zip code retriever"); test("view models should exist", function() { ok(FormViewModel, "A viewModel for our form should exist"); ok(AddressViewModel, "A viewModel for our address should exist"); });
如果你运行这个测试,它会失败,现在你可以编写代码让它通过:
var AddressViewModel = function(options) { }; var FormViewModel = function() { this.address = new AddressViewModel(); };
这次您会看到绿色而不是红色。像这样的测试乍一看有点愚蠢,但它们很有用,因为它们迫使您至少思考设计的一些早期阶段。
我们将编写的下一个测试将适用于 AddressViewModel
的功能。从这个小部件的规范中我们知道,其他字段应该首先隐藏,直到找到邮政编码的数据。
module("address view model"); test("should show city state data if a zip code is found", function() { var address = new AddressViewModel(); ok(!address.isLocated()); address.zip(12345); address.city("foo"); address.state("bar"); address.county("bam"); ok(address.isLocated()); });
尚未编写任何代码,但这里的想法是 isLocated
将是一个计算的可观察值,仅当邮政编码、城市、州和县时才返回 true
都是实话。所以,这个测试一开始当然会失败,现在让我们编写代码让它通过。
var AddressViewModel = function(options) { options = options || {}; this.zip = ko.observable(options.zip); this.city = ko.observable(options.city); this.state = ko.observable(options.state); this.county = ko.observable(options.county); this.isLocated = ko.computed(function() { return this.city() && this.state() && this.county() && this.zip(); }, this); this.initialize(); };
现在,如果您再次运行测试,您将看到绿色!
这是最基本的,如何使用 TDD 编写前端测试。理想情况下,在每次失败的测试之后,您应该编写最简单的代码来使测试通过,然后返回并重构代码。不过,您可以了解更多有关 TDD 实践的知识,因此我建议您进一步阅读并研究它,但前面的示例足以让您考虑首先编写测试。
Sinon.js 是一个 JavaScript 库,提供监视、存根和模拟 JavaScript 对象的功能。编写单元测试时,您希望确保只能测试给定的代码“单元”。这通常意味着您必须对依赖项进行某种模拟或存根以隔离正在测试的代码。
Sinon 有一个非常简单的 API 可以完成此操作。 Geonames API 支持通过 JSONP 端点检索数据,这意味着我们将能够轻松使用 $.ajax
。
理想情况下,您不必在测试中依赖 Geonames API。它们可能会暂时关闭,您的互联网可能会中断,并且实际进行 ajax 调用的速度也会变慢。诗乃前来救援。
test("should only try to get data if there's 5 chars", function() { var address = new AddressViewModel(); sinon.stub(jQuery, "ajax").returns({ done: $.noop }); address.zip(1234); ok(!jQuery.ajax.calledOnce); address.zip(12345); ok(jQuery.ajax.calledOnce); jQuery.ajax.restore(); });
在此测试中,我们做了一些事情。首先,sinon.stub
函数实际上将代理 jQuery.ajax
并添加查看其被调用次数以及许多其他断言的功能。正如测试所示,“应该仅在有 5 个字符时尝试获取数据”,我们假设当地址设置为“1234
”时,尚未进行 ajax 调用,然后将其设置为“12345
”,此时应进行 ajax 调用。
然后我们需要将 jQuery.ajax
恢复到其原始状态,因为我们是单元测试的好公民,并且希望保持我们的测试原子性。保持测试的原子性非常重要,可以确保一个测试不依赖于另一测试,并且测试之间不存在共享状态。然后它们也可以按任何顺序运行。
现在测试已经编写完毕,我们可以运行它,观察它失败,然后编写向 Geonames 执行 ajax 请求的代码。
AddressViewModel.prototype.initialize = function() { this.zip.subscribe(this.zipChanged, this); }; AddressViewModel.prototype.zipChanged = function(value) { if (value.toString().length === 5) { this.fetch(value); } }; AddressViewModel.prototype.fetch = function(zip) { var baseUrl = "http://www.geonames.org/postalCodeLookupJSON" $.ajax({ url: baseUrl, data: { "postalcode": zip, "country": "us" }, type: "GET", dataType: "JSONP" }).done(this.fetched.bind(this)); };
在这里,我们订阅邮政编码的更改。每当它发生变化时,都会调用 zipChanged
方法。 zipChanged
方法将检查 zip 值的长度是否为 5
。当到达 5
时,将调用 fetch
方法。这就是Sinon 存根发挥作用的地方。此时,$.ajax
实际上是一个Sinon存根。因此,在测试中 CalledOnce
将是 true
。
我们将编写的最终测试是数据从 Geonames 服务返回时的情况:
test("should set city info based off search result", function() { var address = new AddressViewModel(); address.fetched({ postalcodes: [{ adminCode1: "foo", adminName2: "bar", placeName: "bam" }] }); equal(address.city(), "bam"); equal(address.state(), "foo"); equal(address.county(), "bar"); });
此测试将测试如何将来自服务器的数据设置到 AddressViewmodel
上。运行一下,看到一些红色。现在将其设为绿色:
AddressViewModel.prototype.fetched = function(data) { var cityInfo; if (data.postalcodes && data.postalcodes.length === 1) { cityInfo = data.postalcodes[0]; this.city(cityInfo.placeName); this.state(cityInfo.adminCode1); this.county(cityInfo.adminName2); } };
fetched方法只是确保从服务器传来的数据中有一个postalcodes
数组,然后在viewModel
上设置相应的属性。
看看这现在有多容易了吗?一旦你掌握了执行此操作的流程,你就会发现自己几乎不想再进行 TDD。您最终会得到可测试的漂亮小函数。您强迫自己思考代码如何与其依赖项交互。现在,当代码中添加其他新需求时,您可以运行一套测试。即使您错过了某些内容并且代码中存在错误,您现在也可以简单地向套件添加新测试,以证明您已经修复了错误!它实际上最终会让人上瘾。
测试覆盖率提供了一种简单的方法来评估单元测试测试了多少代码。达到 100% 的覆盖率通常很困难且不值得,但请尽您所能使其尽可能高。
更新且更简单的覆盖库之一称为 Blanket.js。将它与 QUnit 一起使用非常简单。只需从他们的主页获取代码或使用 Bower 安装即可。然后将毯子添加为 qunit.html
文件底部的库,然后将 data-cover
添加到您想要进行覆盖率测试的所有文件。
<script src="../app/yourSourceCode.js" data-cover></script> <script src="../js/lib/qunit/qunit/qunit.js"></script> <script src="../js/lib/blanket/dist/qunit/blanket.js"></script> <script src="tests.js"></script> </body>
完成。超级简单,现在您将在 QUnit 运行程序中获得一个用于显示覆盖范围的选项:
在此示例中,您可以看到测试覆盖率并不是 100%,但在这种情况下,由于代码不多,因此很容易提高覆盖率。您实际上可以深入了解尚未涵盖的确切功能:
在这种情况下,FormViewModel
从未在测试中实例化,因此缺少测试覆盖率。然后,您可以简单地添加一个新测试来创建 FormViewModel
的实例,并且可能编写一个断言来检查 address
属性是否存在并且是 instanceOf
AddressViewModel
。
然后您将很高兴看到 100% 的测试覆盖率。
随着您的应用程序变得越来越大,能够对 JavaScript 代码运行一些静态分析是件好事。 Plato 是一个在 JavaScript 上运行分析的好工具。
您可以通过 npm
安装来运行 plato
:
npm install -g plato
然后您可以在 JavaScript 代码目录上运行 plato
:
plato -r -d js/app reports
这将在位于“js/app
”的所有 JavaScript 上运行 Plato,并将结果输出到 reports
。 Plato 对您的代码运行各种指标,包括平均代码行数、计算的可维护性分数、JSHint、难度、估计错误等等。
在上一张图片中没有太多可看的内容,仅仅是因为对于我们一直在处理的代码来说,只有一个文件,但是当您开始使用具有大量文件的大型应用程序时,几行代码,您会发现它为您提供的信息非常有用。
它甚至会跟踪您运行它的所有时间,以便您可以查看统计数据如何随时间变化。
虽然测试客户端似乎是一个困难的提议,但现在有很多很棒的工具可以使用,使它变得超级简单。本文仅仅触及了当今所有使客户端测试变得容易的事情的表面。这可能是一项乏味的任务,但最终您会发现,拥有测试套件和可测试代码的好处远远超过它。希望通过此处概述的步骤,您将能够快速开始测试客户端代码。