Jasmine JavaScript 测试(二)
原文:
zh.annas-archive.org/md5/298440D531543CD7EE2CF1AAAB25EE4F译者:飞龙
第五章:Jasmine 间谍
测试替身是单元测试的一种模式。它用一个等效的实现替换测试依赖组件,该实现特定于测试场景。这些实现被称为替身,因为尽管它们的行为可能特定于测试,但它们的行为就像并且具有与其模拟的对象相同的 API。
间谍是 Jasmine 对测试替身的解决方案。在其核心,Jasmine 的spy是一种特殊类型的函数,记录与其发生的所有交互。因此,当返回值或对象状态的变化不能用于确定测试期望是否成功时,它们非常有用。换句话说,当测试成功只能通过行为检查来确定时,Jasmine 间谍是完美的。
“裸”间谍
为了理解行为检查的概念,让我们重新访问第三章中提出的示例,测试前端代码,并测试NewInvestmentView测试套件的可观察行为:
describe("NewInvestmentView", function() {
var view;
// setup and other specs ...
describe("with its inputs correctly filled", function() {
// setup and other specs ...
describe("and when an investment is created", function() {
var callbackSpy;
var investment;
beforeEach(function() {
callbackSpy = **jasmine.createSpy('callback')**;
view.onCreate(callbackSpy);
investment = view.create();
});
it("should invoke the 'onCreate' callback with the created investment", function() {
**expect(callbackSpy).toHaveBeenCalled();**
**expect(callbackSpy).toHaveBeenCalledWith(investment);**
});
});
});
});
在规范设置期间,使用jasmine.createSpy函数创建一个新的 Jasmine 间谍,并为其传递一个名称(callback)。Jasmine 间谍是一种特殊类型的函数,用于跟踪对其进行的调用和参数。
然后,它将这个间谍设置为 View 的 create 事件的观察者,使用onCreate函数,最后调用create函数创建一个新的投资。
随后,在期望中,规范使用toHaveBeenCalled和toHaveBeenCalledWith匹配器来检查callbackSpy是否被调用,并且使用了正确的参数(investment),从而进行行为检查。
对对象的函数进行间谍活动
一个间谍本身非常有用,但它的真正力量在于使用对应的间谍来更改对象的原始实现。
考虑以下示例,旨在验证当提交表单时,必须调用view的create函数:
describe("NewInvestmentView", function() {
var view;
// setup and other specs ...
describe("with its inputs correctly filled", function() {
// setup and other specs ...
describe("and when the form is submitted", function() {
beforeEach(function() {
**spyOn(view, 'create');**
view.$element.submit();
});
it("should create an investment", function() {
**expect(view.create).toHaveBeenCalled();**
});
});
});
});
在这里,我们利用全局 Jasmine 函数spyOn来使用间谍更改view的create函数。
然后,在规范中稍后,我们使用toHaveBeenCalled Jasmine 匹配器来验证view.create函数是否被调用。
规范完成后,Jasmine 会恢复对象的原始行为。
测试 DOM 事件
在编写前端应用程序时,DOM 事件经常被使用,有时我们打算编写一个规范,检查事件是否被触发。
事件可能是表单提交或输入已更改之类的东西,那么我们如何使用间谍来做到这一点呢?
我们可以向NewInvestmentView测试套件添加一个新的验收标准,以检查在单击添加按钮时是否提交了其表单:
describe("and when its add button is clicked", function() {
beforeEach(function() {
**spyOnEvent(view.$element, 'submit');**
view.20.18.find('input[type=submit]').click();
});
it("should submit the form", function() {
**expect('submit').toHaveBeenTriggeredOn(view.20.18);**
});
});
为了编写这个规范,我们使用 Jasmine jQuery 插件提供的spyOnEvent全局函数。
它通过接受view.20.18,这是一个 DOM 元素,以及我们想要监视的submit事件来工作。然后,稍后,我们使用 jasmine jQuery 匹配器toHaveBeenTriggeredOn来检查事件是否在元素上触发。
总结
在本章中,您了解了测试替身的概念以及如何使用间谍来执行规范上的行为检查。
在下一章中,我们将看看如何使用伪造和存根来替换我们规范的真实依赖项,并加快其执行速度。
第六章:光速单元测试
在第四章中,异步测试 - AJAX,我们看到了如何在应用程序中包含 AJAX 测试会增加测试的复杂性。在该章节的示例中,我们创建了一个结果可预测的服务器。基本上是一个复杂的测试装置。即使我们可以使用真实的服务器实现,它也会增加测试的复杂性;尝试从浏览器更改具有数据库或第三方服务的服务器的状态并不是一种容易或可扩展的解决方案。
还有对生产力的影响;这些请求需要时间来处理和传输,这会影响通常提供的快速反馈循环。
您还可以说这些规范测试了客户端和服务器代码,因此不能被视为单元测试;相反,它们可以被视为集成测试。
解决所有这些问题的一个方法是使用存根或虚假来代替代码的真实依赖关系。因此,我们不是向服务器发出请求,而是在浏览器内部使用服务器的测试替身。
我们将使用第四章中的相同示例,异步测试 - AJAX,并使用不同的技术进行重写。
Jasmine 存根
我们已经看到了 Jasmine 间谍的一些用例。记住,间谍是一个特殊的函数,记录了它的调用方式。你可以把存根看作是带有行为的间谍。
我们在想要在规范中强制执行特定路径或替换真实实现为更简单实现时使用存根。
让我们通过使用 Jasmine 存根来重新审视接受标准的示例,“获取股票时,应更新其股价”。
我们知道股票的fetch函数是使用$.getJSON函数实现的,如下所示:
Stock.prototype.fetch = function(parameters) {
**$.getJSON**(url, function (data) {
that.sharePrice = data.sharePrice;
success(that);
});
};
我们可以使用spyOn函数来设置对getJSON函数的间谍,代码如下:
describe("when fetched", function() {
beforeEach(function() {
**spyOn($, 'getJSON').and.callFake(function(url, callback) {**
**callback({ sharePrice: 20.18 });**
**});**
stock.fetch();
});
it("should update its share price", function() {
expect(stock.sharePrice).toEqual(20.18);
});
});
但这一次,我们将使用and.callFake函数为我们的间谍设置行为(默认情况下,间谍什么也不做,返回 undefined)。我们让间谍使用其callback参数调用一个对象响应({ sharePrice: 20.18 })。
随后,在期望中,我们使用toEqual断言来验证股票的sharePrice是否已更改。
要运行此规范,您不再需要服务器来进行请求,这是一件好事,但这种方法存在一个问题。如果fetch函数被重构为使用$.ajax而不是$.getJSON,那么测试将失败。一个更好的解决方案,由 Jasmine 插件jasmine-ajax提供,是代替浏览器的 AJAX 基础设施,因此 AJAX 请求的实现可以以不同的方式进行。
Jasmine Ajax
Jasmine Ajax 是一个官方插件,旨在帮助测试 AJAX 请求。它将浏览器的 AJAX 请求基础设施更改为虚假实现。
这个虚假(或模拟)实现,虽然更简单,但对于使用其 API 的任何代码来说,仍然表现得像真实的实现一样。
安装插件
在深入规范实现之前,首先需要将插件添加到项目中。转到github.com/jasmine/jasmine-ajax/并下载当前版本(应与 Jasmine 2.x 版本兼容)。将其放在lib文件夹中。
它还需要添加到SpecRunner.html文件中,所以继续添加另一个脚本:
<script type="text/javascript" src="lib/mock-ajax.js"></script>
一个虚假的 XMLHttpRequest
每当你使用 jQuery 进行 AJAX 请求时,在幕后实际上是使用XMLHttpRequest对象来执行请求。
XMLHttpRequest是标准的 JavaScript HTTP API。尽管它的名称暗示它使用 XML,但它支持其他类型的内容,比如 JSON;出于兼容性原因,名称保持不变。
因此,我们可以改变XMLHttpRequest对象的假实现,而不是存根 jQuery。这正是这个插件所做的。
让我们重写先前的规范以使用这个虚假实现:
describe("when fetched", function() {
**beforeEach(function() {**
**jasmine.Ajax.install();**
**});**
beforeEach(function() {
stock.fetch();
**jasmine.Ajax.requests.mostRecent().respondWith({**
**'status': 200,**
**'contentType': 'application/json',**
**'responseText': '{ "sharePrice": 20.18 }'**
**});**
});
**afterEach(function() {**
**jasmine.Ajax.uninstall();**
**});**
it("should update its share price", function() {
expect(stock.sharePrice).toEqual(20.18);
});
});
深入实施:
-
首先,我们告诉插件使用
jasmine.Ajax.install函数将XMLHttpRequest对象的原始实现替换为假实现。 -
然后调用
stock.fetch函数,该函数将调用$.getJSON,在幕后创建新的XMLHttpRequest。 -
最后,我们使用
jasmine.Ajax.requests.mostRecent().respondWith函数来获取最近发出的请求,并用假响应对其进行响应。
我们使用respondWith函数,该函数接受一个具有三个属性的对象:
-
status属性用于定义 HTTP 状态码。 -
contentType(在示例中为 JSON)属性。 -
responseText属性,其中包含请求的响应主体的文本字符串。
然后,一切都是运行期望的问题:
it("should update its share price", function() {
expect(stock.sharePrice).toEqual(20.18);
});
由于插件更改了全局的XMLHttpRequest对象,您必须记住告诉 Jasmine 在测试运行后将其恢复到原始实现;否则,您可能会干扰其他规范(例如 Jasmine jQuery fixtures 模块)中的代码。以下是您可以实现这一点的方法:
afterEach(function() {
jasmine.Ajax.uninstall();
});
还有一种略有不同的方法来编写这个规范;在这里,首先对请求进行存根(带有响应细节),然后执行要执行的代码。
先前的示例更改为以下内容:
beforeEach(function() {
**jasmine.Ajax.stubRequest('http://localhost:8000/stocks/AOUE').andReturn({**
**'status': 200,**
**'contentType': 'application/json',**
**'responseText': '{ "sharePrice": 20.18 }'**
**});**
stock.fetch();
});
可以使用jasmine.Ajax.stubRequest函数来存根对特定请求的任何请求。在示例中,它由 URL http://localhost:8000/stocks/AOUE定义,并且响应定义如下:
{
'status': 200,
'contentType': 'application/json',
'responseText': '{ "sharePrice": 20.18 }'
}
响应定义遵循与先前使用的respondWith函数相同的属性。
摘要
在本章中,您了解了异步测试如何影响您可以通过单元测试获得的快速反馈循环。我展示了如何使用存根或假实现使您的规范更快地运行并减少依赖关系。
我们已经看到了两种不同的方式,您可以使用简单的 Jasmine 存根或更高级的XMLHttpRequest的假实现来测试 AJAX 请求。
您还更加熟悉间谍和存根,并应该更加舒适地在不同场景中使用它们。
在下一章中,我们将进一步探讨我们应用程序的复杂性,并进行全面重构,将其转换为一个完全功能的单页面应用程序,使用React.js库。
第七章:测试 React 应用程序
作为 Web 开发人员,您熟悉今天构建大多数网站的方式。通常有一个 Web 服务器(使用 Java、Ruby 或 PHP 等语言),它处理用户请求并响应标记(HTML)。
这意味着在每个请求上,Web 服务器通过 URL 解释用户操作并呈现整个页面。
为了改善用户体验,越来越多的功能开始从服务器端推送到客户端,并且 JavaScript 不再仅仅是为页面添加行为,而是完全渲染页面。最大的优势是用户操作不再触发整个页面刷新;JavaScript 代码可以处理整个浏览器文档并相应地进行变异。
尽管这确实改善了用户体验,但它开始给应用程序代码增加了很多复杂性,导致维护成本增加,最糟糕的是——在屏幕不同部分之间存在不一致的错误形式。
为了使这种情况变得理智,建立了许多库和框架,但它们都失败了,因为它们没有解决整个问题的根本原因——可变性。
服务器端渲染很容易,因为没有变异要处理。给定一个新的应用程序状态,服务器将简单地重新渲染所有内容。如果我们能从这种方法中在客户端 JavaScript 代码中获益会怎样呢?
这正是React提出的。您可以通过组件声明性地编写接口代码,并告诉 React 进行渲染。在应用程序状态发生任何变化时,您可以简单地告诉 React 再次进行重新渲染;然后它将计算移动 DOM 到所需状态所需的变异,并为您应用它们。
在本章中,我们将通过将到目前为止构建的代码重构为 SPA 来了解 React 的工作原理。
项目设置
但是,在我们可以深入了解 React 之前,首先我们需要在我们的项目中进行一些小的设置,以便我们可以创建 React 组件。
转到facebook.github.io/react/downloads.html并下载 React Starter Kit 版本 0.12.2 或更高版本。
下载后,您可以解压其内容,并将构建文件夹中的所有文件移动到我们应用程序的 lib 文件夹中。然后,只需将 React 库加载到SpecRunner.html文件中。
**<script src="lib/react-with-addons.js"></script>**
<script src="lib/jquery.js"></script>
设置完成后,我们可以继续编写我们的第一个组件。
我们的第一个 React 组件
正如本章的介绍所述,使用 React,您可以通过组件声明性地编写接口代码。
React 组件的概念类似于第三章中介绍的组件概念,因此可以期待看到一些相似之处。
有了这个想法,让我们创建我们的第一个组件。为了更好地理解 React 组件是什么,我们将使用一个非常简单的验收标准,并像往常一样从规范开始。
让我们实现"InvestmentListItem 应该呈现"。这很简单,不是真正面向特性,但是是一个很好的例子,让我们开始。
根据我们在第三章中学到的知识,我们可以通过创建一个名为InvestmentListItemSpec.js的新文件并将其保存在spec文件夹内的components文件夹中来开始编写这个规范:
describe("InvestmentListItem", function() {
beforeEach(function() {
// render the React component
});
**it("should render", function() {**
**expect(component.$el).toEqual('li.investment-list-item');**
**});**
});
将新文件添加到SpecRunner.html文件中,就像在之前的章节中已经演示的那样。
在规范中,我们基本上使用jasmine-jquery插件来期望我们组件的封装 DOM 元素等于特定的 CSS 选择器。
我们如何将这个示例更改为 React 组件的测试?唯一的区别是获取 DOM 节点的 API。React 暴露了一个名为getDOMNode()的函数,它返回它所声明的 DOM 节点。
有了这个,我们可以使用与之前相同的断言,并准备好我们的测试,如下所示:
it("should render", function() {
expect(component.**getDOMNode()**).toEqual('li.investment-list-item');
});
那很容易!所以下一步是创建组件,渲染它,并将其附加到文档中。这也很简单;看一下以下要点:
describe("InvestmentListItem", function() {
var component;
beforeEach(function() {
**setFixtures('<div id="application-container"></div>');**
**var container = document.getElementById('application-container');**
**var element = React.createElement(InvestmentListItem);**
**component = React.render(element, container);**
});
it("should render", function() {
expect(component.getDOMNode()).toEqual('li.investment-list-item');
});
});
它可能看起来像是很多代码,但其中一半只是样板文件,用于设置我们可以在其中渲染 React 组件的文档元素装置:
-
首先,我们使用
jasmine-jquery中的setFixtures函数在文档中创建一个带有application-containerID 的元素。然后,使用getElementByIdAPI,我们查询此元素并将其保存在container变量中。接下来的两个步骤是特定于 React 的步骤: -
首先,为了使用组件,我们必须首先从其类创建一个元素;这是通过
React.createElement函数完成的,如下所示:
var element = **React.createElement**(InvestmentListItem);
- 接下来,使用元素实例,我们最终可以通过
React.render函数告诉 React 渲染它,如下所示:
component = **React.render**(element, container);
render函数接受以下两个参数:
-
React 元素
-
要渲染元素的 DOM 节点
- 到目前为止,规范已经完成。您可以运行它并查看它失败,显示以下错误:
ReferenceError: InvestmentListItem is not defined.
-
下一步是编写组件。因此,让我们满足规范,在
src文件夹中创建一个新文件,命名为InvestmentListItem.js,并将其添加到规范运行程序。此文件应遵循我们到目前为止一直在使用的模块模式。 -
然后,使用
React.createClass方法创建一个新的组件类:
(function (React) {
var InvestmentListItem = **React.createClass**({
**render**: function () {
return React.createElement('li', { className: 'investment-list-item' }, 'Investment');
}
});
this.InvestmentListItem = InvestmentListItem;
})(React);
-
至少,
React.createClass方法期望一个应该返回 React 元素树的render函数。 -
我们再次使用
React.createElement方法来创建将成为渲染树根的元素,如下所示:
**React.createElement**('li', { className: 'investment-list-item' }, 'Investment')
与在beforeEach块中以前的用法的不同之处在于,这里还传递了一个props列表(带有className)和包含文本Investment的单个子元素。
我们将更深入地了解 props 参数的含义,但您可以将其视为类似于 HTML DOM 元素的属性。className prop 将变成li元素的 class HTML 属性。
React.createElement方法签名接受三个参数:
-
组件的类型可以是表示真实 DOM 元素的字符串(例如
div,h1,p)或 React 组件类 -
包含 props 值的对象
-
以及可变数量的子组件,在这种情况下,只是
Investment字符串
在渲染此组件(通过调用React.render()方法)时,结果将是:
<li class="investment-list-item">Investment</li>
这是生成它的 JavaScript 代码的直接表示:
React.createElement('li', { className: 'investment-list-item' }, 'Investment');
恭喜!您已经构建了您的第一个完全测试的 React 组件。
虚拟 DOM
当您定义组件的渲染方法并调用React.createElement方法时,您实际上并没有在文档中渲染任何内容(甚至没有创建 DOM 元素)。
只有通过调用这些React.createElement调用创建的表示才能有效地转换为真实的 DOM 元素并附加到文档中。
由ReactElements定义的这种表示是 React 称之为虚拟 DOM。ReactElement不应与 DOM 元素混淆;它实际上是 DOM 元素的轻量、无状态、不可变的虚拟表示。
那么为什么 React 要费力地创建一种新的表示 DOM 的方式呢?答案在于性能。
随着浏览器的发展,JavaScript 的性能不断提高,如今的应用程序瓶颈实际上并不是 JavaScript。您可能听说过应该尽量少地触及 DOM,而 React 允许您通过让您与其自己的 DOM 版本交互来做到这一点。
然而,这并不是唯一的原因。React 构建了一个非常强大的差异算法,可以比较虚拟 DOM 的两个不同表示,计算它们的差异,并根据这些信息创建变化,然后应用于真实 DOM。
它使我们能够回到以前在服务器端渲染中使用的流程。基本上,我们可以在应用程序状态的任何更改时要求 React 重新渲染所有内容,然后它将计算所需的最小更改数量,并仅将其应用于真实 DOM。
它使我们开发人员不必担心改变 DOM,并赋予我们以声明方式编写用户界面的能力,同时减少错误并提高生产力。
JSX
如果您有编写前端 JavaScript 应用程序的经验,您可能熟悉一些模板语言。此时,您可能想知道在哪里可以使用您喜欢的模板语言(如 Handlebars)与 React 一起使用。答案是不能。
React 不会区分标记和逻辑;在 React 组件中,它们实际上是相同的。
然而,当我们开始创建更复杂的组件时会发生什么?我们在第三章中构建的表单会如何转换为 React 组件?
要仅呈现它而没有其他逻辑,需要进行一系列的React.createElement调用,如下所示:
var NewInvestment = React.createClass({
render: function () {
return React.createElement("form", {className: "new-investment"},
React.createElement("h1", null, "New investment"),
React.createElement("label", null,
"Symbol:",
React.createElement("input", {type: "text", className: "new-investment-stock-symbol", maxLength: "4"})
),
React.createElement("label", null,
"Shares:",
React.createElement("input", {type: "number", className: "new-investment-shares"})
),
React.createElement("label", null,
"Share price:",
React.createElement("input", {type: "number", className: "new-investment-share-price"})
),
React.createElement("input", {type: "submit", className: "new-investment-submit", value: "Add"})
);
}
});
这非常冗长且难以阅读。因此,考虑到 React 组件既是标记又是逻辑,如果我们能够将其编写为 HTML 和 JavaScript 的混合,那不是更好吗?下面是方法:
var NewInvestment = React.createClass({
render: function () {
return <form className="new-investment">
<h1>New investment</h1>
<label>
Symbol:
<input type="text" className="new-investment-stock-symbol" maxLength="4" />
</label>
<label>
Shares:
<input type="number" className="new-investment-shares" />
</label>
<label>
Share price:
<input type="number" className="new-investment-share-price" />
</label>
<input type="submit" className="new-investment-submit" value="Add" />
</form>;
}
});
这就是JSX,一种看起来像 XML 的 JavaScript 语法扩展,专为与 React 一起使用而构建。
它会转换为 JavaScript,因此,根据后面的示例,它将直接编译为之前呈现的普通 JavaScript 代码。
转换过程的一个重要特性是它不会改变行号;因此,在 JSX 中的第 10 行将转换为转换后的 JavaScript 文件中的第 10 行。这有助于调试代码和进行静态代码分析。
有关该语言的更多信息,您可以在facebook.github.io/jsx/上查看官方规范,但现在,您可以随着我们深入了解该语言的特性,跟随下面的示例。
重要的是要知道,在实现 React 组件时并不要求使用 JSX,但它会让这个过程变得更容易。考虑到这一点,我们暂时会继续使用它。
使用 JSX 与 Jasmine
为了让我们能够在 Jasmine 运行器中使用 JSX,我们需要做一些更改。
首先,我们需要将要使用 JSX 语法的文件重命名为.jsx。虽然这不是必需的,但它可以让我们轻松地识别出文件是否使用了这种特殊语法。
接下来,在SpecRunner.html文件中,我们需要更改脚本标签,以指示这些不是常规的 JavaScript 文件,如下所示:
<script src="src/components/InvestmentListItem**.jsx**" **type="text/jsx"**></script>
<script src="spec/components/InvestmentListItemSpec**.jsx**" **type="text/jsx"**></script>
不幸的是,这些不是我们需要做的唯一更改。浏览器无法理解 JSX 语法,因此我们需要加载一个特殊的转换器,首先将这些文件转换为常规的 JavaScript。
这个转换器已经捆绑在 React 起始套件中,所以只需在加载 React 后立即加载它,如下所示:
<script src="lib/react-with-addons.js"></script>
**<script src="lib/JSXTransformer.js"></script>**
完成此设置后,我们应该能够运行测试,不是吗?不幸的是,还有一步。
如果您尝试在浏览器中打开SpecRunner.html文件,您会发现InvestmentListItem的测试没有被执行。这是因为转换器通过 AJAX 加载脚本文件,对其进行转换,最后将其附加到 DOM。在此过程完成时,Jasmine 已经运行了测试。
我们需要一种方法来告诉 Jasmine 等待这些文件加载和转换。
最简单的方法是更改jasmine-2.1.3文件夹中lib文件夹内的jasmine的boot.js文件。
在原始文件中,你需要找到包含env.execute();方法的行并将其注释掉。它应该类似于以下代码:
window.onload = function() {
if (currentWindowOnload) {
currentWindowOnload();
}
htmlReporter.initialize();
**// delays execution so that JSX files can be loaded**
**// env.execute();**
};
文件中的其他内容应该保持不变。在这个更改之后,你会发现测试不再运行——一个都没有。
唯一缺失的部分是一旦加载了 JSX 文件就调用这个execute方法。为此,我们将在jasmine.2.1.3文件夹中创建一个名为boot-exec.js的新文件,内容如下:
/**
Custom boot file that actually runs the tests.
The code below was extracted and commented out from the original boot.js file.
*/
(function() {
var env = jasmine.getEnv();
env.execute();
}());
正如你所看到的,它只是执行原始引导文件中以前注释的代码。
运行这个自定义引导非常简单。我们将它作为 JSX 类型添加到SpecRunner.html的<head>标签的最后一行:
**<!-- After all JSX files were loaded and processed, the tests can finally run -->**
**<script src="lib/jasmine-2.1.3/boot-exec.js" type="text/jsx"></script>**
</head>
JSXTransformer库保证脚本按声明的顺序加载。因此,当boot-exec.js文件加载时,源文件和测试文件已经加载完毕。
有了这个,我们的测试运行器现在支持 JSX 了。
组件属性(props)
Props 是在 React 中从父组件传递数据到子组件的方式。
对于下一个示例,我们想要更改InvestmentListItem组件,以便以百分比格式呈现roi变量的值。
为了实现下一个规范,我们将使用 React 通过React.addons.TestUtils对象提供的一些辅助方法,如下所示:
describe("InvestmentListItem", function() {
var TestUtils = React.addons.TestUtils;
describe("given an Investment", function() {
var investment, component;
beforeEach(function() {
investment = new Investment({
stock: new Stock({ symbol: 'peto', sharePrice: 0.25 }),
shares: 100,
sharePrice: 0.20
});
component = **TestUtils.renderIntoDocument**(
<InvestmentListItem investment={investment}/>
);
});
it("should render the return of investment as a percentage", function() {
var roi = **TestUtils.findRenderedDOMComponentWithClass**(component, 'roi');
expect(roi.getDOMNode()).toHaveText('25%');
});
});
});
如你所见,我们不再使用jasmine-jquery匹配器中的setFixture方法。相反,我们使用TestUtils模块来渲染组件。
这里最大的区别是TestUtils.renderIntoDocument实际上并没有在文档中渲染,而是渲染到一个分离的节点中。
你将注意到的下一件事是InvestmentListItem组件有一个属性(实际上称为prop),我们通过它传递investment。
然后,在规范中,我们使用另一个名为findRenderedDOMComponentWithClass的辅助方法来查找component变量中的 DOM 元素。
这个方法返回ReactElement。然后,我们将使用getDOMNode方法获取实际的 DOM 元素,然后使用jasmine-jquery匹配器来检查其文本值,如下所示:
var roi = TestUtils.findRenderedDOMComponentWithClass(component, 'roi');
expect(roi.getDOMNode()).**toHaveText**('25%');
在组件中实现这种行为实际上非常简单:
(function (React) {
var InvestmentListItem = React.createClass({
render: function () {
var investment = **this.props.investment**;
return <li className="investment-list-item">
<article>
<span className="roi">**{formatPercentage(investment.roi())}**</span>
</article>
</li>;
}
});
function formatPercentage (number) {
return (number * 100).toFixed(0) + '%';
}
this.InvestmentListItem = InvestmentListItem;
})(React);
我们可以通过this.props对象访问传递给组件的任何 props。
扩展原始实现,我们添加了一个带有规范中预期类的span元素。
为了使投资回报率动态化,JSX 有一种特殊的语法。使用{},你可以在 XML 中间调用任何 JavaScript 代码。我们在传递investment.roi()值时调用formatPercentage函数,如下所示:
<span className="roi">{formatPercentage(investment.roi())}</span>
再次强调一下,这个 JSX 转换成 JavaScript 将是:
React.createElement("span", {className: "roi"}, formatPercentage(investment.roi()))
重要的是要知道,prop 应该是不可变的。改变自己的 prop 值不是组件的责任。你可以将只有 props 的 React 组件视为纯函数,因为它总是在给定相同参数值的情况下返回相同的结果值。
这使得测试非常简单,因为没有变异或更改状态来测试组件。
组件事件
UI 应用程序有用户事件;在 Web 中,它们以 DOM 事件的形式出现。由于 React 将每个 DOM 元素包装成 React 元素,处理它们会有一点不同,但非常熟悉。
对于下一个示例,假设我们的应用程序允许用户删除一个投资。我们可以通过以下验收标准来表达这个要求:
给定一个投资,当单击删除按钮时,InvestmentListItem 应该通知观察者 onClickDelete。
这里的想法与第三章中的将视图与观察者集成部分中提出的想法是一样的,测试前端代码。
那么,我们应该如何在组件中设置观察者?正如我们之前已经看到的,props是将属性传递给我们的组件的方式,如下所示:
describe("InvestmentListItem", function() {
var TestUtils = React.addons.TestUtils;
describe("given an Investment", function() {
var investment, component, onClickDelete;
beforeEach(function() {
investment = new Investment({
stock: new Stock({ symbol: 'peto', sharePrice: 0.25 }),
shares: 100,
sharePrice: 0.20
});
onClickDelete = jasmine.createSpy('onClickDelete');
component = TestUtils.renderIntoDocument(
<InvestmentListItem investment={investment} **onClickDelete={onClickDelete}**/>
);
});
it("should notify an observer onClickDelete when the delete button is clicked", function() {
var deleteButton = TestUtils.findRenderedDOMComponentWithTag(component, 'button');
TestUtils.Simulate.click(deleteButton);
expect(onClickDelete).toHaveBeenCalled();
});
});
});
正如你所看到的,我们将另一个 prop 传递给onClickDelete组件,并将其值设置为 Jasmine spy,如下所示:
**onClickDelete = jasmine.createSpy('onClickDelete');**
component = TestUtils.renderIntoDocument(
<InvestmentListItem investment={investment} **onClickDelete={onClickDelete}**
/>
);
然后,我们通过其标签找到了删除按钮,并使用TestUtils模块模拟了一个点击,期望之前创建的间谍被调用,如下所示:
var deleteButton = TestUtils.findRenderedDOMComponentWithTag(component, 'button');
TestUtils.Simulate.click(deleteButton);
expect(onClickDelete).toHaveBeenCalled();
TestUtils.Simulate模块包含了模拟所有类型的 DOM 事件的辅助方法,如下所示:
TestUtils.Simulate.**click**(node);
TestUtils.Simulate.**change**(node, {target: {value: 'Hello, world'}});
TestUtils.Simulate.**keyDown**(node, {key: "Enter"});
然后,我们回到了实现:
(function (React) {
var InvestmentListItem = React.createClass({
render: function () {
var investment = this.props.investment;
**var onClickDelete = this.props.onClickDelete;**
return <li className="investment-list-item">
<article>
<span className="roi">{formatPercentage(investment.roi())}</span>
<button className="delete-investment" **onClick={onClickDelete}**>Delete</button>
</article>
</li>;
}
});
function formatPercentage (number) {
return (number * 100).toFixed(0) + '%';
}
this.InvestmentListItem = InvestmentListItem;
})(React);
正如你所看到的,它就像嵌套另一个button组件并将onClickDelete属性值作为其onClick属性传递一样简单。
React 标准化事件,以便它们在不同浏览器中具有一致的属性,但其命名约定和语法类似于 HTML 中的内联 JavaScript 代码。要获取支持的事件的全面列表,可以在官方文档中查看facebook.github.io/react/docs/events.html。
#组件状态
到目前为止,我们已经将 React 视为一个无状态的渲染引擎,但是我们知道,应用程序有状态,特别是在使用表单时。那么,我们应该如何实现NewInvestment组件,以便它保持正在创建的投资的值,然后在用户完成表单后通知观察者?
为了帮助我们实现这种行为,我们将使用另一个组件内部 API——它的state。
让我们看一下以下验收标准:
鉴于NewInvestment组件的输入已正确填写,当提交表单时,它应该使用投资属性通知onCreate观察者:
describe("NewInvestment", function() {
var TestUtils = React.addons.TestUtils;
var component, onCreateSpy;
function findNodeWithClass (className) {
return TestUtils.findRenderedDOMComponentWithClass(component, className).getDOMNode();
}
beforeEach(function() {
onCreateSpy = jasmine.createSpy('onCreateSpy');
component = TestUtils.renderIntoDocument(
<NewInvestment onCreate={onCreateSpy}/>
);
});
describe("with its inputs correctly filled", function() {
beforeEach(function() {
var stockSymbol = findNodeWithClass('new-investment-stock-symbol');
var shares = findNodeWithClass('new-investment-shares');
var sharePrice = findNodeWithClass('new-investment-share-price');
TestUtils.Simulate.change(stockSymbol, { target: { value: 'AOUE' }});
TestUtils.Simulate.change(shares, { target: { value: '100' }});
TestUtils.Simulate.change(sharePrice, { target: { value: '20' }});
});
describe("when its form is submitted", function() {
beforeEach(function() {
var form = component.getDOMNode();
TestUtils.Simulate.submit(form);
});
it("should invoke the 'onCreate' callback with the investment attributes", function() {
var investmentAttributes = { stockSymbol: 'AOUE', shares: '100', sharePrice: '20' };
expect(onCreateSpy).toHaveBeenCalledWith(investmentAttributes);
});
});
});
});
这个规范基本上使用了我们到目前为止学到的所有技巧,所以不要深入细节,让我们直接进入组件实现。
任何具有状态的组件必须声明的第一件事是通过定义getInitialState方法来定义其初始状态,如下所示:
var NewInvestment = React.createClass({
**getInitialState: function () {**
**return {**
**stockSymbol: '',**
**shares: 0,**
**sharePrice: 0**
**};**
**},**
render: function () {
**var state = this.state;**
return <form className="new-investment">
<h1>New investment</h1>
<label>
Symbol:
<input type="text" ref="stockSymbol" className="new-investment-stock-symbol" **value={state.stockSymbol}** maxLength="4"/>
</label>
<label>
Shares:
<input type="number" className="new-investment-shares" **value={state.shares}**/>
</label>
<label>
Share price:
<input type="number" className="new-investment-share-price" **value={state.sharePrice}**/>
</label>
<input type="submit" className="new-investment-submit" value="Add"/>
</form>;
}
});
正如前面的代码所示,我们清楚地定义了表单的初始状态,并在渲染方法中将状态作为value属性传递给输入组件。
如果您在浏览器中运行此示例,您会注意到您无法更改输入的值。您可以聚焦在输入上,但尝试输入不会更改其值,这是因为 React 的工作方式。
与 HTML 不同,React 组件必须在任何时间点表示视图的状态,而不仅仅是在初始化时。如果我们想要更改输入的值,我们需要监听输入的onChange事件,并根据该信息更新状态。状态的更改将触发渲染,从而更新屏幕上的值。
为了演示这是如何工作的,让我们在stockSymbol输入中实现这种行为。
首先,我们需要更改渲染方法,为onChange事件添加一个处理程序:
<input type="text" ref="stockSymbol" className="new-investment-stock-symbol" value={state.stockSymbol} maxLength="4" **onChange={this._handleStockSymbolChange}**/>
一旦触发事件,它将调用_handleStockSymbolChange方法。它的实现应该通过调用this.setState方法来更新状态,新的输入值如下所示:
var NewInvestment = React.createClass({
getInitialState: function () {
// ... Method implementation
},
render: function () {
// ... Method implementation
},
**_handleStockSymbolChange: function (event) {**
**this.setState({ stockSymbol: event.target.value });**
**}**
});
事件处理程序是在将输入数据传递给状态之前执行简单验证或转换的好地方。
正如你所看到的,这是大量样板代码,只是为了处理单个输入。由于我们没有在事件处理程序中实现任何自定义行为,我们可以使用特殊的 React 功能来为我们实现这个“链接状态”。
我们将使用一个名为LinkedStateMixin的Mixin;但首先,什么是 Mixin?它是在组件之间共享常见功能的一种方式,这种情况下是“链接状态”。看一下以下代码:
var NewInvestment = React.createClass({
**mixins: [React.addons.LinkedStateMixin],**
// ...
render: function () {
// ...
<input type="text" ref="stockSymbol" className="new-investment-stock-symbol" **valueLink={this.linkState('stockSymbol')}** maxLength="4" />
// ...
}
});
LinkedStateMixin通过向组件添加linkState函数工作,而不是设置输入的value,我们使用由函数this.linkState返回的链接对象设置一个名为valueLink的特殊属性。
linkState函数期望state的属性名称,它应该将其链接到输入的值。
组件生命周期
你可能已经注意到,React 对组件的 API 有自己的看法。但它对组件的生命周期也有非常强烈的看法,允许我们开发人员添加钩子来创建自定义行为并在开发组件时执行清理任务。
这是 React 的最大胜利之一,因为通过这种标准化,我们可以通过组合创建更大更好的组件;通过这样,我们不仅可以使用我们自己的组件,还可以使用其他人的组件。
为了演示一个用例,我们将实现一个非常简单的行为:在页面加载时,我们希望新的投资表单股票符号输入获得焦点,以便用户可以立即开始输入。
但是,在我们开始编写测试之前,有一件事情我们需要做。如前所述,TestUtils.renderIntoDocument实际上并不在文档中呈现任何内容,而是在一个分离的节点上呈现。因此,如果我们使用它来呈现我们的组件,我们将无法对输入的焦点进行断言。
因此,我们必须再次使用setFixtures方法来实际在文档中呈现 React 组件,如下所示:
/**
Uses jasmine-jquery fixtures to actually render in the document.
React.TestUtils.renderIntoDocument renders in a detached node.
This was required to test the focus behavior.
*/
function actuallyRender (component) {
setFixtures('<div id="application-container"></div>');
var container = document.getElementById('application-container');
return React.render(component, container);
}
describe("NewInvestment", function() {
var TestUtils = React.addons.TestUtils;
var component, stockSymbol;
function findNodeWithClass (className) {
return TestUtils.findRenderedDOMComponentWithClass(component, className).getDOMNode();
}
beforeEach(function() {
component = actuallyRender(<NewInvestment onCreate={onCreateSpy}/>);
stockSymbol = findNodeWithClass('new-investment-stock-symbol');
});
it("should have its stock symbol input on focus", function() {
expect(stockSymbol).toBeFocused();
});
});
完成了这个小改变,并编写了规范,我们可以回到实现中。
React 提供了一些钩子,我们可以在组件的生命周期中实现自定义代码;它们如下:
-
componentWillMount -
componentDidMount -
componentWillReceiveProps -
shouldComponentUpdate -
componentWillUpdate -
componentDidUpdate -
componentWillUnmount
为了实现我们的自定义行为,我们将使用componentDidMount钩子,该钩子仅在组件被呈现并附加到 DOM 元素后调用一次。
因此,我们想要在这个钩子内部以某种方式访问输入 DOM 元素并触发其焦点。我们已经知道如何获取 DOM 节点;通过getDOMNode API。但是,我们如何获取输入的 React 元素呢?
React 针对这个问题的另一个特性称为ref。基本上,它是一种为组件的子元素命名的方法,以允许以后访问。
由于我们想要股票符号输入,我们需要向其添加一个ref属性,如下所示:
<input type="text" **ref="stockSymbol"** className="new-investment-stock-symbol" valueLink={this.linkState('stockSymbol')} maxLength="4" />
然后,在componentDidMount钩子中,我们可以通过其ref名称获取输入,然后获取其 DOM 元素并触发焦点,如下所示:
var NewInvestment = React.createClass({
// ...
**componentDidMount: function () {**
**this.refs.stockSymbol.getDOMNode().focus();**
**}**
,
// ...
});
其他钩子以相同的方式设置,只需在类定义对象上定义它们作为属性。但是每个钩子在不同的场合被调用,并且有不同的规则。官方文档是关于它们定义和可能用例的很好资源,可以在facebook.github.io/react/docs/component-specs.html#lifecycle-methods找到。
组合组件
我们已经谈了很多关于通过组合 React 的默认组件来创建组件的可组合性。然而,我们还没有展示如何将自定义组件组合到更大的组件中。
你可能已经猜到,这应该是一个非常简单的练习,为了演示这个工作原理,我们将实现一个列出投资的组件,如下所示:
var InvestmentList = React.createClass({
render: function () {
var onClickDelete = this.props.onClickDelete;
var listItems = this.props.investments.map(function (investment) {
return <**InvestmentListItem** investment={investment}
onClickDelete={onClickDelete.bind(null, investment)}/>;
});
return <ul className="investment-list">{listItems}</ul>;
}
});
只需使用已经可用的InvestmentListItem全局变量作为InvestmentList组件的根元素即可。
该组件期望investments属性是一个投资数组。然后,它通过为数组中的每个投资创建一个InvestmentListItem元素来映射它。
最后,它使用listItems数组作为ul元素的子元素,有效地定义了如何呈现投资列表。
摘要
React 是一个快速发展的库,受到 JavaScript 社区的广泛关注;它引入了一些有趣的模式,并质疑了一些既定的教条,不断改进了丰富的 Web 应用程序的开发。
本章的目标不是深入了解这个库,而是概述其主要特性和理念。它证明了在使用 React 编写界面时可以进行测试驱动开发。
你学到了prop和state以及它们的区别:prop不是组件所拥有的,如果需要,应该由其父组件进行更改。state是组件拥有的数据。它可以被组件更改,这样就会触发新的渲染。
在你的应用程序中,拥有状态的组件越少,就越容易理解和测试。
通过 React 的有主见的 API 和生命周期,我们可以最大程度地实现组合性和代码重用的好处。
当你开始使用 React 进行应用程序开发时,建议你了解 Flux,这是 Facebook 推荐的构建应用程序的架构,网址是facebook.github.io/flux/。
第八章:构建自动化
我们看到如何使用 Jasmine 从头开始创建应用程序。然而,随着应用程序的增长和文件数量的增加,管理它们之间的依赖关系可能会变得有点困难。
例如,我们在 Investment 和 Stock 模型之间有一个依赖关系,它们必须按正确的顺序加载才能工作。因此,我们尽力而为;我们按照脚本的加载顺序进行排序,以便在加载 Investment 后 Stock 可用。下面是我们的做法:
<script type="text/javascript" src="src/Stock.js"></"script>
<script type="text/javascript" src="src/Investment.js"></"script>
然而,这很快就会变得繁琐和难以管理。
另一个问题是应用程序用于加载所有文件的请求数量;一旦应用程序开始增长,这个数量可能会增加到数百个。
因此,我们在这里有一个悖论;尽管将其分解为小模块有利于代码的可维护性,但对于客户端性能来说却是不利的,单个文件更加可取。
在同一时间满足以下两个要求将是完美的:
-
在开发中,我们有一堆包含不同模块的小文件
-
在生产中,我们有一个包含所有这些模块内容的单个文件
显然,我们需要一些构建过程。有许多不同的方法可以用 JavaScript 实现这些目标,但我们将专注于webpack。
模块捆绑器 - webpack
Webpack 是由 Tobias Koppers 创建的模块捆绑器,用于帮助创建大型和模块化的前端 JavaScript 应用程序。
它与其他解决方案的主要区别在于它支持任何类型的模块系统(AMD 和 CommonJS)、语言(CoffeeScript、TypeScript 和 JSX)甚至通过加载器支持资产(图像和模板)。
你没看错,甚至包括图片;如果在 React 应用中,一切都是组件,在 webpack 项目中,一切都是模块。
它构建了所有资产的依赖图,在开发环境中为它们提供服务,并在生产环境中对它们进行优化。
模块定义
JavaScript 是一种基于 ECMA 脚本规范的语言,直到第 6 版,仍没有对模块的标准定义。这种缺乏正式标准导致了许多竞争的社区标准(AMD 和 CommonJS)和实现(RequireJS 和 browserify)。
现在,有一个标准可供遵循,但不幸的是,现代浏览器不支持它,那么我们应该使用哪种样式来编写我们的模块呢?
好消息是,通过转译器,我们可以今天就使用 ES6,这给了我们未来的优势。
一个流行的转译器是Babel(babeljs.io/),我们将通过一个加载器与 webpack 一起使用它。
我们将看到如何在 webpack 中使用它,但首先重要的是要理解什么是 ES6 模块。这是一个简单的定义,没有任何依赖:
function MyModule () {};
export default MyModule;
让我们将其与我们到目前为止一直声明模块的方式进行比较。下一个示例显示了如果使用第三章中介绍的约定编写的代码将会是什么样子,测试前端代码:
(function () {
function MyModule() {};
this.MyModule = MyModule;
}());
最大的区别在于缺少 IIFE。ES6 模块默认具有自己的作用域,因此不可能意外地污染全局命名空间。
第二个区别是模块值不再附加到全局对象上,而是作为默认模块值导出:
function MyModule () {};
**export default MyModule;**
关于模块的依赖关系,到目前为止,一切都是全局可用的,因此我们将依赖项作为参数传递给 IIFE 模块,如下所示:
(function (**$**) {
function MyModule() {};
this.MyModule = MyModule;
}(**jQuery**));
然而,一旦在项目中开始使用 ES6 模块,就不再有全局变量了。那么,如何将这些依赖项引入模块呢?
如果你还记得之前的内容,ES6 示例是通过export default语法导出模块值的。因此,给定一个模块有一个值,我们所要做的就是将其作为依赖项请求。让我们将 jQuery 依赖项添加到我们的 ES6 模块中:
**import $ from 'jQuery';**
function MyModule () {};
export default MyModule;
这里,$代表依赖项将加载到的变量名,jQuery是文件名。
也可以导出多个值作为模块的结果,并将这些值导入到不同的变量中,但是在本书的范围内,默认值就足够了。
ES6 标准引入了许多不同的构造到 JavaScript 语言中,这些内容超出了本书的范围。更多信息,请查看 Babel 的优秀文档babeljs.io/docs/learn-es6/。
Webpack 项目设置
Webpack 可以作为一个 NPM 包使用,它的设置非常简单,将在接下来的章节中进行演示。
提示
重要的是要理解 NPM 和 Node.js 之间的区别。NPM 既是一个包管理器,也是一个包格式,而 Node.js 是 NPM 模块通常运行的平台。
使用 NPM 管理依赖关系
我们已经有了一个 Node.js 项目的雏形,但是在本章中我们将开始使用更多的依赖项,因此我们需要一个正式的定义,列出项目所依赖的所有 NPM 包。
为了将项目定义为一个 NPM 包,同时定义所有的依赖项,我们需要在应用程序的根文件夹中创建一个名为package.json的特殊文件。可以通过一个简单的命令轻松创建它:
**npm init**
它将提示一系列关于项目的问题,所有这些问题都可以保持默认值。最后,你应该有一个类似以下输出的文件,具体取决于你的文件夹名称:
{
"name": "jasmine-testing-project",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts":" {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
Is this ok? (Yes)
下一步是安装我们所有的依赖项,目前只有 express。
**npm install --save express**
前面的命令不仅会安装 express,如第四章中所述,异步测试 - AJAX,还会将其添加为package.json文件的依赖项。在之前运行npm init命令时,我们得到了以下输出,显示了dependencies属性:
{
"name": "jasmine-testing-project",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
**"dependencies": {**
**"express": "⁴.12.0"**
**}**
}
现在我们了解了如何管理项目的依赖关系,我们可以安装webpack和Babel作为开发依赖项,以开始打包我们的模块,如下所示:
**npm install --save-dev babel-loader webpack webpack-dev-server**
最后一步是在package.json中添加一个脚本来启动开发服务器:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
**"dev": "webpack-dev-server"**
}
这使我们可以通过一个简单的命令启动开发服务器:
**npm run dev**
webpack-dev-server可执行文件的实际位置在./node_modules/.bin文件夹中。因此,npm run dev等同于:
**./node_modules/.bin/webpack-dev-server**
这是因为当你运行npm run <scriptName>时,NPM 会将./node_modules/.bin文件夹添加到路径中。
Webpack 配置
接下来,我们需要配置 webpack,让它知道要捆绑哪些文件。可以通过在项目的根文件夹中创建一个名为webpack.config.js的文件来实现。它的内容应该是:
module.exports = {
context: __dirname,
entry: {
spec: [
'./spec/StockSpec.js',
'./spec/InvestmentSpec.js',
'./spec/components/NewInvestmentSpec.jsx',
'./spec/components/InvestmentListItemSpec.jsx',
'./spec/components/InvestmentListSpec.jsx'
]
},
output: {
filename: '[name].js'
},
module: {
loaders: [
{
test: /(\.js)|(\.jsx)$/,
exclude: /node_modules/,
loader: 'babel-loader'
}
]
}
};
关于这个配置文件有一些关键点:
-
context指令告诉 webpack 在__dirname中查找模块,意思是项目的根文件夹。 -
entry指令指定了应用程序的入口点。由于我们目前只是在进行测试,所以只有一个名为spec的入口点,它指向我们所有的规范文件。 -
output.filename指令用于指定每个入口点的文件名。[name]模式将在编译时被入口点名称替换。因此,spec.js实际上将包含我们所有的规范代码。 -
module.loaders最后一条指令告诉 webpack 如何处理不同的文件类型。我们在这里使用babel-loader参数来为我们的源文件添加对 ES6 模块和 JSX 语法的支持。exclude指令很重要,以免编译node_modules文件夹中的任何依赖项。
完成了这个设置后,你可以启动开发服务器,并在http://localhost:8080/spec.js上检查转译后的捆绑文件的样子(在配置文件中定义的文件名)。
此时,webpack 配置已经完成,我们可以开始适应 Jasmine 运行器以运行规范。
规范运行器
如前所述,我们正在使用 webpack 来编译和捆绑源文件,因此 Jasmine 规范将变得简单得多:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Jasmine Spec Runner v2.1.3</title>
<link rel="shortcut icon" type="image/png" href="lib/jasmine-2.1.3/jasmine_favicon.png">
<link rel="stylesheet" href="lib/jasmine-2.1.3/jasmine.css">
<script src="lib/jasmine-2.1.3/jasmine.js"></script>
<script src="lib/jasmine-2.1.3/jasmine-html.js"></script>
<script src="lib/jasmine-2.1.3/boot.js"></script>
<script src="lib/jquery.js"></script>
<script src="lib/jasmine-jquery.js"></script>
<script src="lib/mock-ajax.js"></script>
<script src="spec/SpecHelper.js"></script>
**<script src="spec.js"></script>**
</head>
<body>
</body>
</html>
有一些要点:
首先,我们不再需要在前一章中解释的 JSX 转换器 hack;转换现在由 webpack 和 babel-loader 完成。因此,我们可以很好地使用默认的 Jasmine boot。
其次,我们选择将测试运行器依赖项保留为全局(Jasmine,Mock Ajax,Jasmine JQuery 和 Spec helper)。将它们保留为全局对于我们的测试运行器来说会简单得多,并且就模块化而言,我们不会伤害我们的代码。
此刻,尝试在http://localhost:8080/SpecRunner.html上运行测试应该会因缺少引用而产生许多失败。这是因为我们仍然需要将我们的规范和源转换为 ES6 模块。
测试一个模块
要能够运行所有测试,需要将所有源文件和规范文件转换为 ES6 模块。在规范中,这意味着将所有源模块添加为依赖项:
**import Stock from '../src/Stock';**
describe("Stock", function() {
// the original spec code
});
在源文件中,这意味着声明所有依赖项并导出其默认值,如下所示:
**import React from 'react';**
var InvestmentListItem = React.createClass({
// original code
});
**export default InvestmentListItem;**
一旦所有代码都转换完成,启动开发服务器并再次将浏览器指向运行器 URL 后,测试应该可以正常工作。
测试运行器:Karma
还记得我们在介绍中说过,我们可以执行 Jasmine 而不需要浏览器窗口吗?为此,我们将使用PhantomJS,一个可编写脚本的无头 WebKit 浏览器(驱动 Safari 浏览器的相同渲染引擎)和Karma,一个测试运行器。
设置非常简单;再次使用 NPM 安装一些依赖项:
**npm install –save-dev karma karma-jasmine karma-webpack karma-phantomjs-launcher es5-shim**
这里唯一奇怪的依赖是es5-shim,它用于为 PhantomJS 提供对一些 ES5 功能的支持,而 PhantomJS 仍然缺少这些功能,React 需要。
下一步是在项目的根文件夹中创建一个名为karma.conf.js的配置文件,用于 Karma。
module.exports = function(config) {
config.set({
basePath: '.',
frameworks: ['jasmine'],
browsers: ['PhantomJS'],
files: [
// shim to workaroud PhantomJS 1.x lack of 'bind' support
// see: https://github.com/ariya/phantomjs/issues/10522
'node_modules/es5-shim/es5-shim.js',
'lib/jquery.js',
'lib/jasmine-jquery.js',
'lib/mock-ajax.js',
'spec/SpecHelper.js',
'spec/**/*Spec.*'
],
preprocessors: {
'spec/**/*Spec.*': ['webpack']
},
webpack: require('./webpack.config.js'),
webpackServer: { noInfo: true },
singleRun: true
});
};
在其中,我们设置了 Jasmine 框架和 PhantomJS 浏览器:
frameworks: [**'jasmine'**],
browsers: [**'PhantomJS'**],
通过加载es5-shim来修复 PhantomJS 上的浏览器兼容性问题,如下所示:
// shim to workaroud PhantomJS 1.x lack of 'bind' support
// see: https://github.com/ariya/phantomjs/issues/10522
**'node_modules/es5-shim/es5-shim.js'**,
加载先前在SpecRunner.html文件中全局的测试运行器依赖项,如下所示:
'lib/jquery.js',
'lib/jasmine-jquery.js',
'lib/mock-ajax.js',
'spec/SpecHelper.js',
最后,加载所有规范,如下所示:
'spec/**/*Spec.*',
到目前为止,您可以删除SpecRunner.html文件,webpack.config.js文件中的规范条目以及lib/jasmine-2.1.3文件夹。
通过调用 Karma 来运行测试,在控制台中打印测试结果,如下所示:
**./node_modules/karma/bin/karma start karma.conf.js**
**> investment-tracker@0.0.1 test /Users/paulo/Dropbox/jasmine_book/second_edition/book/chapter_8/code/webpack-karma**
**> ./node_modules/karma/bin/karma start karma.conf.js**
**INFO [karma]: Karma v0.12.31 server started at http://localhost:9876/**
**INFO [launcher]: Starting browser PhantomJS**
**INFO [PhantomJS 1.9.8 (Mac OS X)]: Connected on socket cGbcpcpaDgX14wdyzLZh with id 37309028**
**PhantomJS 1.9.8 (Mac OS X): Executed 36 of 36 SUCCESS (0.21 secs / 0.247 secs)**
为了更简单地运行测试,可以更改package.json项目文件并描述其测试脚本:
"scripts": {
**"test": "./node_modules/karma/bin/karma start karma.conf.js",**
"dev": "webpack-dev-server"
},
然后,您可以通过简单调用以下命令来运行测试:
**npm test**
快速反馈循环
自动化测试的关键在于快速反馈循环,因此想象一下能够在控制台中运行测试并在任何文件更改后刷新应用程序的浏览器。这可能吗?答案是肯定的!
观看并运行测试
通过在启动 Karma 时添加一个简单的参数,我们可以实现测试的极乐世界,如下所示:
**./node_modules/karma/bin/karma start karma.conf.js --auto-watch --no-single-run**
自己尝试一下;运行此命令,更改文件,然后查看测试是否自动运行-就像魔术一样。
再次,我们不想记住这些复杂的命令,因此让我们向package.json文件添加另一个脚本:
"scripts": {
"test": "./node_modules/karma/bin/karma start karma.conf.js",
**"watch-test": "./node_modules/karma/bin/karma start karma.conf.js --auto-watch --no-single-run",**
"dev": "webpack-dev-server"
},
我们可以通过以下命令运行它:
**npm run watch-test**
观看并更新浏览器
为了实现开发的极乐世界,我们也只差一个参数。
在启动开发服务器时,将以下内容添加到package.json文件中:
./node_modules/.bin/webpack-dev-server **--inline –hot**
再次在浏览器上尝试一下;更改文本编辑器中的文件,浏览器应该会刷新。
还鼓励您使用这些新参数更新package.json文件,以便运行npm run dev可以获得“实时重新加载”的好处。
为生产进行优化
我们模块打包目标的最后一步是生成一个经过缩小并准备好生产的文件。
大部分配置已经完成,只缺少几个步骤。
第一步是为应用程序设置一个入口点,然后将一个启动所有内容的索引文件index.js放在src文件夹中,内容如下:
import React from 'react';
import Application from './Application.jsx';
var mountNode = document.getElementById('application-container''');
React.render(React.createElement(Application, {}), mountNode);
我们在书中没有详细介绍这个文件的实现,所以一定要检查附加的源文件,以更好地理解它的工作原理。
在 webpack 配置文件中,我们需要添加一个输出路径来指示捆绑文件将放置在哪里,以及我们刚刚创建的新入口文件,如下所示:
module.exports = {
context: __dirname,
**entry: {**
**index: './src/index.js'**
**},**
output: {
**path: 'dist',**
filename: '[name]-[hash].js'
},
module: {
loaders: [
{
test: /(\.js)|(\.jsx)$/,
exclude: /node_modules/,
loader: 'babel-loader'
}
]
}
};
然后,剩下的就是在我们的package.json文件中创建一个构建任务:
"scripts": {
"test": "./node_modules/karma/bin/karma start karma.conf.js",
"watch-test": "./node_modules/karma/bin/karma start karma.conf.js --auto-watch --no-single-run",
**"build": "webpack -p",**
"dev": "webpack-dev-server --inline --hot"
},
运行它并检查dist文件夹中的构建文件,如下所示:
**npm run build**
静态代码分析:JSHint
正如第一章所述,JavaScript 不是一种编译语言,但运行代码(如自动化测试)并不是检查错误的唯一方法。
一整类工具能够读取源文件,解释它们,并查找常见错误或不良实践,而无需实际运行源文件。
一个非常流行的工具是JSHint—一个简单的二进制文件,也可以通过 NPM 安装,如下所示:
npm install --save-dev **jshint jsxhint**
您可以看到我们还安装了JSXHint,另一个用于执行 JSX 文件的静态分析的工具。它基本上是原始 JSHint 的包装器,同时执行 JSX 转换。
如果你还记得上一章,JSXTransformer 不会改变行号,所以 JavaScript 文件中给定行号上的警告将在原始 JSX 文件中的相同行号上。
执行它们非常简单,如下所示:
./node_modules/.bin/jshint .
./node_modules/.bin/jsxhint .
然而,每当我们运行测试时,让它们运行也是一个好主意:
"scripts": {
"start": "node bin/server.js",
"test": **"./node_modules/.bin/jshint . && ./node_modules/.bin/jsxhint . &&** ./node_modules/karma/bin/karma start karma.conf.js",
"watch-test": "./node_modules/karma/bin/karma start karma.conf.js --auto-watch --no-single-run",
"build": "webpack -p",
"dev": "webpack-dev-server --inline --hot"
},
最后一步是配置我们希望 JSHint 和 JSXHint 捕获的错误。再次,在项目的根文件夹中创建另一个配置文件,这次名为.jshintrc:
{
"esnext": true,
"undef": true,
"unused": true,
"indent": 2,
"noempty": true,
"browser": true,
"node": true,
"globals": {
"jasmine": false,
"spyOn": false,
"describe": false,
"beforeEach": false,
"afterEach": false,
"expect": false,
"it": false,
"xit": false,
"setFixtures": false
}
}
这是一个启用或禁用的选项标志列表,其中最重要的是以下内容:
-
esnext:此标志告诉我们我们正在使用 ES6 版本 -
unused:此标志会在任何未使用的声明变量上中断 -
undef:此选项标志会在使用未声明的变量时中断
还有一个globals变量列表,用于测试,以防止由于undef标志而出现错误。
前往 JSHint 网站jshint.com/docs/options/查看完整的选项列表。
唯一缺少的步骤是防止 linter 在其他人的代码(Jasmine,React 等)中运行。这可以通过简单地创建一个文件来实现,该文件应包含应忽略的文件夹。这个名为.jshintignore的文件应包含:
-
node_modules -
lib
现在运行静态分析和所有测试就像这样简单:
**npm test**
持续集成-Travis-CI
我们已经为项目创建了大量自动化,这对于团队中新开发人员的入职非常有帮助;运行测试只需两个命令:
**npm install**
**npm test**
然而,这不是唯一的优势;我们可以在持续集成环境中通过这两个命令运行测试。
为了演示可能的设置,我们将使用 Travis-CI(travis-ci.org),这是一个免费的开源项目解决方案。
在我们开始之前,需要您拥有一个 GitHub(github.com/)帐户,并且项目已经托管在那里。我希望您已经熟悉 git(www.git-scm.com/)和 GitHub。
一旦准备好,我们就可以开始 Travis-CI 的设置。
添加项目到 Travis-CI
在我们可以为项目添加 Travis-CI 支持之前,首先需要将项目添加到 Travis-CI。
转到 Travis-CI 网站travis-ci.org,然后点击右上角的使用 GitHub 登录。
输入您的 GitHub 凭据,一旦您登录,它应该显示您所有存储库的列表:
如果您的存储库没有显示出来,您可以点击立即同步按钮,让 Travis-CI 更新列表。
一旦您的存储库出现,点击开关启用它。这将在您的 GitHub 项目上设置钩子,因此 Travis-CI 会在对存储库进行任何更改时收到通知。
项目设置
设置 Travis-CI 项目再简单不过了。因为我们的构建过程和测试都已经脚本化,我们所要做的就是告诉 Travis-CI 应该使用什么运行时。
Travis-CI 知道 Node.js 项目依赖项是通过npm install安装的,并且测试是通过npm test运行的,因此没有额外的步骤来运行我们的测试。
在项目根目录创建一个名为.travis.yml的新文件,并将 Travis 的语言配置为 Node.js:
language: node_js
就是这样。
使用 Travis-CI 的步骤非常简单,将这些概念应用到其他持续集成环境,比如 Jenkins(jenkins-ci.org/)应该也很简单。
总结
在本章中,我希望向您展示自动化的力量,以及我们如何使用脚本来使生活更轻松。您了解了 webpack 以及它如何用于管理模块之间的依赖关系,并帮助您生成生产代码(打包和压缩)。
静态代码分析的力量帮助我们在代码运行之前甚至找到错误。
您还看到了如何在无头模式下甚至自动运行您的规范,让您始终专注于代码编辑器。
最后,我们已经看到了使用持续集成环境是多么简单,以及我们如何使用这个强大的概念来保持我们的项目始终得到测试。