ホームページ > 記事 > ウェブフロントエンド > クライアントを守ることを忘れないでください!
アプリケーションを確実にテストすることで、コード内で見つかるエラーの数を減らし、アプリケーションの保守性を向上させ、適切に構造化されたコードを設計できます。
クライアント側の単体テストには、サーバー側のテストとは異なる課題があります。クライアント側のコードを扱う場合、アプリケーション ロジックを DOM ロジックから分離するのに苦労し、JavaScript コードをビルドするだけになることがよくあります。幸いなことに、コードのテスト、テスト カバレッジに関するメトリクスの作成、複雑さの分析に役立つ、優れたクライアント側テスト ライブラリが数多くあります。
まず第一に、単体テストは多くの場合、アプリケーションが期待どおりに動作することを確認することでエラーを減らす方法です。これとは別に、テスト駆動開発 (TDD) と動作駆動開発 (BDD) という概念もあります。
これら 2 つの単体テスト戦略は、アプリケーション ロジックを作成する前にテストを作成することで、アプリケーションの設計に役立ちます。コードを記述する前にテストを記述することで、アプリケーションの設計について慎重に考える機会が得られます。
これが起こるのは、テストを作成するときは、基本的にコードが対話する API を設計しようとするため、その設計をより深く理解できるようになります。あなたが書いているテストコードは基本的にあなたが書いているコードを使用するので、最初にテストすると設計の欠陥がすぐに明らかになります。
TDD はコード検出プロセスです
TDD はコードを作成しながら発見するのに役立つことがわかります。 TDD は、すぐに「レッド、グリーン、リファクタリング」として要約できます。これは、テストを作成するときに、最初にテストが fail になるのに十分なコードを作成することを意味します。 次に、テストに合格するコードを作成します。その後、今書いたことをよく考えて再構築します。素敵で簡単です。
BDD は TDD とは少し異なり、ビジネス ニーズと仕様に基づいています。
クライアント コードをテストする必要がある理由はたくさんあります。前述したように、これはエラーを減らし、アプリケーションの設計に役立ちます。クライアント側のテストは、プレゼンテーションから離れてフロントエンド コードを独立してテストする機会を与えるため、また重要です。言い換えれば、その利点の 1 つは、実際にアプリケーション サーバーを起動せずに JavaScript コードをテストできることです。クリックしてテストする代わりに、テストを実行して機能が動作することを確認するだけで済みます。多くの場合、テストを正しく設定している限り、インターネット アクセスは必要ありません。
JavaScript は現代の Web 開発において非常に重要な役割を果たしているため、コードをテストする方法を学び、本番コードにバグが侵入する可能性を減らすことが重要です。あなたの上司はこのようなことが起こることを望んでいませんし、あなたもそうすべきではありません。実際、クライアント側のテストを始めるには、エラー報告を中心としたテストを作成するのが良いでしょう。これにより、最初から始める場所がない場合でも、テストを書く練習をすることができます。
クライアント コードをテストするもう 1 つの理由は、一連のテストが存在し、コードを適切にカバーできるようになると、コードに新しい機能を追加する準備ができたときに、新しい機能を再度追加できるようになることです。テストを実行して、既存の機能が低下したり壊れたりしていないことを確認してください。
これまでクライアント テストを行ったことがない場合、クライアント テストを始めるのは難しいかもしれません。クライアント側のテストで最も難しい部分の 1 つは、DOM をアプリケーション ロジックから分離する最適な方法を見つけることです。これは通常、DOM に対して何らかの抽象化が必要であることを意味します。これを実現する最も簡単な方法は、いくつか例を挙げると、Knockout.js、Backbone.js、Angular.js などのクライアント側フレームワークを使用することです。
このようなライブラリを使用すると、ページがブラウザーでどのようにレンダリングされるかについて考えるのではなく、アプリケーションの機能について考えることができます。ただし、単純な JavaScript を使用した単体テストは不可能ではありません。この場合、DOM を簡単に抽象化できるようにコードを設計すると、作業がはるかに楽になります。
さまざまなテスト ライブラリから選択できますが、代表的な 3 つは QUnit、Mocha、Jasmine になる傾向があります。
Jasmine と Mocha は両方とも BDD 単体テスト学校から来ていますが、QUnit は独自の単体テスト フレームワークにすぎません。
QUnit はクライアント テストへの参入障壁が非常に低いため、この記事の残りの部分では QUnit の使用について検討します。詳細については、QUnit の詳細な紹介を参照してください。
QUnit を始めるのはとても簡単です。必要なのは次の HTML だけです:
リーリー次のいくつかの例では、テキスト ボックスに郵便番号を入力し、Geonames を使用して対応する都市、州、郡の値を返すウィジェットを構築しているとします。最初は郵便番号のみが表示されますが、郵便番号が 5 文字になると、Geonames からデータが取得されます。データが見つかると、結果の都市、州、郡の情報を含むフィールドがさらに表示されます。 Knockout.js も使用します。最初のステップは、失敗するテストを作成することです。
最初のテストを作成する前に、設計について少し考えてください。おそらく少なくとも 2 つの 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 中国語 Web サイトの他の関連記事を参照してください。