애플리케이션을 테스트하면 코드에서 발견되는 버그 수를 줄이고 애플리케이션의 유지 관리성을 향상하며 잘 구조화된 코드를 설계할 수 있습니다.
클라이언트 측 유닛 테스트는 서버 측 테스트와는 다른 과제를 제시합니다. 클라이언트 측 코드로 작업할 때 애플리케이션 로직을 DOM 로직에서 분리하는 데 어려움을 겪고 종종 JavaScript 코드만 작성하는 경우가 있습니다. 다행스럽게도 코드를 테스트하고, 테스트 적용 범위에 대한 지표를 생성하고, 복잡성을 분석하는 데 도움이 되는 훌륭한 클라이언트 측 테스트 라이브러리가 많이 있습니다.
우선, 단위 테스트는 애플리케이션이 예상대로 작동하는지 확인하여 오류를 줄이는 방법인 경우가 많습니다. 이 외에도 테스트 주도 개발(TDD)과 행동 주도 개발(BDD)이라는 개념도 있습니다.
이 두 가지 단위 테스트 전략은 애플리케이션 로직을 작성하기 전에 테스트를 작성하여 애플리케이션을 설계하는 데 도움이 됩니다. 코드를 작성하기 전에 테스트를 작성하면 애플리케이션 디자인에 대해 신중하게 생각할 기회를 갖게 됩니다.
이는 테스트를 작성할 때 기본적으로 코드와 상호 작용하는 API를 설계하여 설계를 더 잘 이해할 수 있기 때문에 발생합니다. 작성 중인 테스트 코드는 본질적으로 작성 중인 코드를 사용하기 때문에 먼저 테스트하면 디자인의 모든 결함이 빠르게 드러납니다!
TDD는 코드 검색 프로세스입니다
TDD가 코드를 작성하면서 코드를 발견하는 데 도움이 된다는 점을 배우게 됩니다. TDD는 "Red, Green, Refactor"로 빠르게 요약될 수 있습니다. 즉, 테스트를 작성하고 애초에 테스트를 실패할 수 있을 만큼 충분한 코드를 작성한다는 의미입니다. 그런 다음 테스트를 통과하는 코드를 작성합니다. 그런 다음 방금 작성한 내용에 대해 신중하게 생각하고 재구성합니다. 좋고 쉽습니다.
BDD는 TDD와 약간 다르며 비즈니스 요구 사항 및 사양을 기반으로 합니다.
클라이언트 코드를 테스트해야 하는 데는 여러 가지 이유가 있습니다. 앞서 언급했듯이 오류를 줄이고 애플리케이션을 설계하는 데 도움이 됩니다. 클라이언트 측 테스트는 프레젠테이션과 별도로 프런트 엔드 코드를 독립적으로 테스트할 수 있는 기회를 제공하므로 중요합니다. 즉, 실제로 애플리케이션 서버를 시작하지 않고도 JavaScript 코드를 테스트할 수 있다는 장점 중 하나가 있습니다. 여기저기 클릭해서 테스트하는 대신 테스트를 실행하고 기능이 작동하는지 확인할 수 있습니다. 많은 경우, 테스트를 올바르게 설정하기만 하면 인터넷 접속이 필요하지 않습니다.
JavaScript가 현대 웹 개발에서 중요한 역할을 하기 때문에 코드를 테스트하고 프로덕션 코드에 버그가 유입될 가능성을 줄이는 방법을 배우는 것이 중요합니다. 당신의 상사는 이런 일이 일어나는 것을 좋아하지 않으며 당신도 마찬가지입니다! 실제로 클라이언트 측 테스트를 시작하기 좋은 곳은 오류 보고에 관한 테스트를 작성하는 것입니다. 이렇게 하면 처음부터 시작할 곳이 없을 때 작문 테스트를 연습할 수 있습니다.
클라이언트 코드를 테스트하는 또 다른 이유는 테스트 모음이 존재하고 코드에 대한 적절한 적용 범위가 확보되면 새 기능을 추가하고 코드에 새 기능을 추가할 준비가 되면 테스트를 다시 실행할 수 있다는 것입니다. 기존 기능이 저하되거나 중단되지 않도록 하세요.
클라이언트 테스트를 한 번도 해본 적이 없다면 클라이언트 테스트를 시작하는 것이 어려울 수 있습니다. 클라이언트 측 테스트에서 가장 어려운 부분 중 하나는 애플리케이션 로직에서 DOM을 분리하는 최선의 방법을 찾는 것입니다. 이는 일반적으로 DOM에 대한 일종의 추상화가 필요함을 의미합니다. 이를 달성하는 가장 쉬운 방법은 Knockout.js, Backbone.js 또는 Angular.js와 같은 클라이언트측 프레임워크를 사용하는 것입니다.
이와 같은 라이브러리를 사용하면 브라우저에서 페이지가 렌더링되는 방식보다는 애플리케이션의 기능에 대해 더 많이 생각할 수 있습니다. 그러나 간단한 JavaScript를 사용한 단위 테스트는 불가능하지 않습니다. 이 경우 DOM을 쉽게 추상화할 수 있는 방식으로 코드를 설계하면 작업이 훨씬 쉬워집니다.
선택할 수 있는 다양한 테스트 라이브러리가 있지만 세 가지 리더는 주로 QUnit, Mocha 및 Jasmine입니다.
Jasmine과 Mocha는 모두 BDD 단위 테스트 학교 출신인 반면 QUnit은 자체 단위 테스트 프레임워크일 뿐입니다.
이 기사의 나머지 부분에서는 클라이언트 테스트 진입 장벽이 매우 낮은 QUnit을 사용하는 방법을 살펴보겠습니다. 자세한 내용은 QUnit의 자세한 소개를 참조하세요.
QUnit을 시작하는 것은 매우 쉽습니다. 필요한 것은 다음 HTML뿐입니다:
으아아아다음 몇 가지 예에서는 텍스트 상자에 우편번호를 입력하고 Geonames를 사용하여 해당 도시, 주, 카운티 값을 반환하는 위젯을 구축한다고 가정해 보겠습니다. 처음에는 우편번호만 표시하지만 우편번호가 5자가 되면 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 중국어 웹사이트의 기타 관련 기사를 참조하세요!