By ensuring that your application is tested, you can reduce the number of errors found in your code, improve the maintainability of your application, and design well-structured code.
Client-side unit testing presents different challenges than server-side testing. When working with client-side code, you'll find yourself struggling to separate application logic from DOM logic, and often just build JavaScript code. Fortunately, there are many great client-side testing libraries that can help test your code, create metrics about test coverage, and analyze its complexity.
First of all, unit testing is often a way to reduce errors by ensuring that the application behaves as expected. Apart from this, there are also the concepts of Test Driven Development (TDD) and Behavior Driven Development (BDD).
These two unit testing strategies will help you design your application by writing tests before writing the application logic. By writing tests before writing code, you will have the opportunity to think carefully about the design of your application.
This happens because when you write tests, you are basically trying to design the API that the code interacts with, so you can better understand its design. Testing first will quickly reveal any flaws in the design because the test code you are writing essentially uses the code you are writing!
TDD is a code discovery process
You'll learn that TDD helps you discover code as you write it. TDD can quickly be summarized as "Red, Green, Refactor." This means that, you write a test, write enough code to make the test fail in the first place. Then, you write the code that makes the test pass. Afterwards, you think carefully about what you just wrote and reconstruct it. Nice and easy.
BDD is slightly different from TDD and is based more on business requirements and specifications.
There are many reasons why you should test your client code. As mentioned before, it will help reduce errors and help you design your application. Client-side testing is also important because it gives you the opportunity to test the front-end code independently, away from the presentation. In other words, one of its advantages is that you can test JavaScript code without actually starting the application server. Instead of clicking around and testing, you can just run the tests and make sure the functionality works. In many cases, you don't even need internet access as long as you set up your test correctly.
With JavaScript playing such an important role in modern web development, it's important to learn how to test your code and reduce the chance of bugs making their way into production code. Your boss doesn’t like this to happen, and neither should you! In fact, a good place to start with client-side testing is by writing your tests around error reporting. This will allow you to practice writing tests when you don't have anywhere to start from scratch.
Another reason to test your client code is that once a set of tests exists and you have appropriate coverage of your code, when you are ready to add new functionality to your code, you will be able to add new functionality, re-run your Test and make sure you don't degrade or break any existing functionality.
If you have never done client testing before, getting started with client testing can be daunting. One of the hardest parts of client-side testing is figuring out the best way to isolate the DOM from application logic. This usually means you need some kind of abstraction over the DOM. The easiest way to achieve this is through a client-side framework such as Knockout.js, Backbone.js, or Angular.js, just to name a few.
When using such a library, you can think less about how your page renders in the browser and more about the functionality of your application. However, unit testing with simple JavaScript is not impossible. In this case, your life will be much easier if you design your code in a way that the DOM can be easily abstracted.
There are many different testing libraries to choose from, although the three leaders tend to be QUnit, Mocha, and Jasmine.
Jasmine and Mocha both come from the BDD unit testing school, while QUnit is just a unit testing framework of its own.
In the remainder of this article, we will explore using QUnit because it has a very low barrier to entry into client testing. See QUnit's detailed introduction for more information.
Getting started with QUnit is very easy. All you need is the following 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>
For the next few examples, let's say we're building a widget that lets you enter a zip code into a text box and it returns the corresponding city, state, and county values using Geonames. It only displays the zip code at first, but once the zip code has five characters, it retrieves data from Geonames. If the data can be found, it will display more fields containing the resulting city, state, and county information. We will also use Knockout.js. The first step is to write tests that fail.
Think about the design a little before writing your first test, you'll probably need at least two viewModels, so this would be a good starting point. First, we'll define a QUnit module and our first test:
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、难度、估计错误等等。
在上一张图片中没有太多可看的内容,仅仅是因为对于我们一直在处理的代码来说,只有一个文件,但是当您开始使用具有大量文件的大型应用程序时,几行代码,您会发现它为您提供的信息非常有用。
它甚至会跟踪您运行它的所有时间,以便您可以查看统计数据如何随时间变化。
虽然测试客户端似乎是一个困难的提议,但现在有很多很棒的工具可以使用,使它变得超级简单。本文仅仅触及了当今所有使客户端测试变得容易的事情的表面。这可能是一项乏味的任务,但最终您会发现,拥有测试套件和可测试代码的好处远远超过它。希望通过此处概述的步骤,您将能够快速开始测试客户端代码。
The above is the detailed content of Remember to protect your clients!. For more information, please follow other related articles on the PHP Chinese website!