如果你正准备新建一个Angular项目,建议你使用Protractor,它将在不久之后取代现在的模块成为端到端测试的内置模块。
现在的应用程序在大小和复杂度方面与日俱增,依靠人工测试来验证新特性、查找bug和进行回归测试已经变得不现实了。
为了解决这个问题,我们创建了Angular场景测试工具(Angular Scenario Runner),它将模拟用户的交互,以帮助你为Angular应用程序进行“体检”。
你可以用JavaScript来写场景测试,它描述你的应用程序的工作方式,在各种特定的状态下定义出正确的互动结果。
一个场景由一个或多个it块语句组成(可以把这些看做应用程序的“需求”),这些语句由命令(command)和期望(expectation)组成。
命令负责告诉测试工具(runner)应用程序去做点什么(比如访问一个页面或者点击一个按钮),而期望则告诉测试工具,这个状态下哪些断言(assertion)必须成立(比如某个输入框的值或者当前页面的URL)。
如果某些期望落空了,测试工具就会把相应的it语句标记为“失败(failed)”,然后继续执行下一个。
场景(scenarios)还可能包含beforeEach和afterEach语句,它们将在每个it语句之前(或之后)运行 —— 无论这些语句是执行成功了还是失败了。

除了上面这些内容之外,场景还可能包含一些辅助函数,用来消除各个it语句中的重复代码。
下面是一个简单的场景范例:
describe('Buzz客户端', function() {
it('应该对结果进行过滤', function() {
input('user').enter('jacksparrow');
element(':button').click();
expect(repeater('ul li').count()).toEqual(10);
input('filterText').enter('Bees');
expect(repeater('ul li').count()).toEqual(1);
});
});
注意:input('user')语句会查找具有ng-model="user"属性的<input>元素,而不会查找具有name="user"属性的!
这个场景描述了一个Buzz客户端的需求,特别是,他应该能够根据用户的输入进行过滤。它先模拟把一个值输入到带有ng-model="user"属性的输入框中,然后,点击页面中唯一的一个按钮,然后,检查是否列出了10条结果。接下来,它在带有ng-model="filterText"属性的输入框中输入文本:'Bees',最后验证一下这个列表是否已经被过滤得只剩下一条结果。
下面列出了在测试工具(runner)的命令(command)和期望(expectation)语句中可以使用的API。
源码:https://github.com/angular/angular.js/blob/master/src/ngScenario/dsl.js
暂停测试代码的执行,直到你在控制台中调用了resume()函数(或者在测试工具的UI中点击了“恢复(resume)”链接)
让测试代码的执行暂停seconds秒。
把指定的url加载到测试框架(译注:测试框架是指用来运行测试的浏览器环境,比如chrome浏览器或phantomjs)中。
把fn返回的URL加载到测试框架中。此处指定的url参数对代码没有实际影响,只用于测试输出。如果目标URL是动态生成的,这种形式会非常有用(即:目标URL在我们写测试的时候是预先无法确定的)。
刷新测试框架中的当前页。
返回测试框架中当前页的window.location.href值。
返回测试框架中当前页的window.location.pathname值。
返回测试框架中当前页的window.location.search值。
返回测试框架中当前页的window.location.hash值(不包括#)。
返回测试框架中当前页的$location.url()值。
返回测试框架中当前页的$location.path()值。
返回测试框架中当前页的$location.search()值。
返回测试框架中当前页的$location.hash()值。
断言future参数(译注:future就是异步执行模式中的通知对象,会在异步执行完毕时触发回调,类似于$q中的promise)的“值(value)”符合匹配器(matcher)的期望。所有API语句都会返回future对象,它被执行后会返回一个“值”。匹配器是通过angular.scenario.matcher定义的,并且通过求出这个future对象的“值”来验证是否符合期望。比如:expect(browser().location().href()).toEqual('http://www.google.com')。后面的文档中将深入讲解各种可用的匹配器。
断言future的值不满足matcher的要求
限定接下来的语句中元素选择器的所属范围。
(译注:这里的选择器 - selector都是指jQuery选择器,参见jQuery选择器)
返回匹配指定name的第一个绑定对象(binding)的值。
(译注:什么是binding?比如假设模板中有'
在ng-model值是name的文本框中输入指定的value。
选中或反选ng-model值是name的检查框。
在ng-model值是name的单选组中选中值为value的那个。
返回ng-model值是name的输入框的当前值。
返回selector选定的repeater(译注:可以简单的理解为ng-repeat)的行数。label参数对代码没有实际影响,只用做测试输出。
返回selector选定的repeater中,绑定到第index行的所有绑定对象(译注:各个绑定对象的值,参见binding函数,一行一般都有多个绑定表达式)构成的数组。label参数对代码没有实际影响,只用做测试输出。
返回selector选定的repeater中,由所有绑定到binding(译注:传入绑定对象的表达式,比如模板中是a,此处就应该用'name + "a"'作为参数,表达式中+前后的空格会被忽略)的列内容构成的数组。label参数对代码没有实际影响,只用做测试输出。
从ng-model值是name的select元素中,选择指定value值的option。
从ng-model值是name的select元素中,选择所有存在于values(即:value1, value2...)参数中的option。
返回selector选定的元素的数量。label参数对代码没有实际影响,只用做测试输出。
模拟点击selector选定的元素。label参数对代码没有实际影响,只用做测试输出。
用fn(selectedElements, done)的形式调用fn函数,selectedElements是匹配指定选择器的所有元素,done是一个函数,供fn结束时调用。label参数对代码没有实际影响,只用做测试输出。
返回在selector选定的元素上调用method()的结果。method可以是下列jquery方法之一:val, text, html, height,
innerHeight, outerHeight, width, innerWidth, outerWidth, position, scrollLeft,
scrollTop, offset。label参数对代码没有实际影响,只用做测试输出。
在selector选定的元素上执行method(value)函数。method可以是下列jquery方法之一:val, text, html, height,
innerHeight, outerHeight, width, innerWidth, outerWidth, position, scrollLeft,
scrollTop, offset。label参数对代码没有实际影响,只用做测试输出。
在selector选定的元素上执行method(key)函数。method可以是下列jquery方法之一:attr, prop, css。label参数对代码没有实际影响,只用做测试输出。
在selector选定的元素上执行method(key, value)函数。method可以是下列jquery方法之一:attr, prop, css。label参数对代码没有实际影响,只用做测试输出。
匹配器(matcher)用于和expect(...)函数组合起来构成断言,并且可以和not()连用来表示否定。例如:expect(element('h1').text()).not().toEqual('Error')。
源码: https://github.com/angular/angular.js/blob/master/src/ngScenario/matchers.js
// 值和对象的比较使用与angular.equals相同的规则
expect(value).toEqual(value)
// 简单类型的比较使用===运算符进行精确比较
expect(value).toBe(value)
// 检查value当前是否具有任何已定义的类型(即value !== undefined)
expect(value).toBeDefined()
// 这两个匹配器使用JavaScript标准的真值规则进行判断
expect(value).toBeTruthy()
expect(value).toBeFalsy()
// 检查value是否符合指定的正则表达式。正则表达式既可以用字符串的形式传入,也可以用正则表达式对象的形式(如new RegExp('.*')或/.*/)传入。
expect(value).toMatch(expectedRegExp)
// 使用===精确检查null值
expect(value).toBeNull()
// 内部用Array.indexOf(...)函数检查指定的元素是否包含在当前数组中。
expect(value).toContain(expected)
// 使用`<`和`>`运算符进行数值比较。
expect(value).toBeLessThan(expected)
expect(value).toBeGreaterThan(expected)
参见Angular种子项目项目中的例子。
Angular场景化的端到端(E2E)测试,高度支持异步特性,它通过将动作和期望存入队列来隐藏了处理异步结果(future)时的很多复杂度。
你可能需要使用一些有条件的断言或元素选取规则,或者需要某些通用的机制来消除重复代码(重复代码往往表示测试代码中有“坏味道”),这时,你可以通过element(...).query(fn)来添加一些有条件的行为。
下列代码将演示这个函数如何通过应用程序的web界面来删除附加的实体(这里的实体是一些领域对象)。
假设应用程序是由两个视图组成的:
beforeEach(function () {
var deleteEntry = function () {
browser().navigateTo('/entries');
// 我们需要选择<tbody>元素,他现在没有实体(即:没有<tr>元素)。如果选择器没有匹配到结果,则本测试直接失败。
element('table tbody').query(function (tbody, done) {
// ngScenario传给我们的是一个jQuery lite包装之后的元素。我们可以调用它的children()函数获取tbody的所有<tr>元素。
var children = tbody.children();
if (children.length > 0) {
// 如果表格中至少有一个实体,点击链接,转到这个实体的详情页
element('table tbody a').click();
// 路由变化之后,点击“删除”按钮。
element('.btn-danger').click();
}
// 如果表格中显示了不止一个实体,则把其他的删除操作排入队列。
if (children.length > 1) {
deleteEntry();
}
// 别忘了调用`done()`函数,这样ngScenario才会继续执行测试,否则会出现超时错误。
done();
});
};
// 开始删除实体
deleteEntry();
});
// 为了帮助理解它的工作原理,我们要强调一句:ngScenario的调用不是立刻执行的,而是先排入队列(按照ngScenario中的术语,我们称之为添加“未来动作(future action)”)。如果在表格中我们只有一个实体,那么下列“未来动作”将被排入队列:
// 删除实体1
browser().navigateTo('/entries');
element('table tbody').query(function (tbody, done) { ... });
element('table tbody a');
element('.btn-danger').click();
对于两个实体,ngScenario将产生下列队列:
// 删除实体1
browser().navigateTo('/entries');
element('table tbody').query(function (tbody, done) { ... });
element('table tbody a');
element('.btn-danger').click();
// 删除实体2
// 这里的缩进排版用来表示递归调用的层数
browser().navigateTo('/entries');
element('table tbody').query(function (tbody, done) { ... });
element('table tbody a');
element('.btn-danger').click();
ngScenario不能和通过调用angular.bootstrap来实现的手动初始化协同工作。你必须使用ng-app指令来启动应用程序。