透過確保您的應用程式經過測試,您可以減少程式碼中發現的錯誤數量,提高應用程式的可維護性,並設計結構良好的程式碼。
客戶端單元測試提出了與伺服器端測試不同的挑戰。在處理客戶端程式碼時,您會發現自己很難將應用程式邏輯與 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、难度、估计错误等等。
在上一张图片中没有太多可看的内容,仅仅是因为对于我们一直在处理的代码来说,只有一个文件,但是当您开始使用具有大量文件的大型应用程序时,几行代码,您会发现它为您提供的信息非常有用。
它甚至会跟踪您运行它的所有时间,以便您可以查看统计数据如何随时间变化。
虽然测试客户端似乎是一个困难的提议,但现在有很多很棒的工具可以使用,使它变得超级简单。本文仅仅触及了当今所有使客户端测试变得容易的事情的表面。这可能是一项乏味的任务,但最终您会发现,拥有测试套件和可测试代码的好处远远超过它。希望通过此处概述的步骤,您将能够快速开始测试客户端代码。
以上是請記得保護您的客戶端!的詳細內容。更多資訊請關注PHP中文網其他相關文章!