Testing Concetps
翻译于一个国外收费的ionic学习网站,原地址就不好贴出来了。
自动化测试出现了很久了,有很多关于测试的概念和最好的实践。尽管某个特定的语言会被用来描述tests,但是相同的概念可以用到任何你在用的编程语言中。
AAA: Arrange, Act, Assert
这也许是需要你理解的重点概念,是指导你创建测试的原则。创建测试代码时,有如下几个清晰简明的步骤:
- Arrange 布置,安排地明明白白
- Act 执行
- Assert 断言
首先,我们需要布置好测试所需要的状态,然后执行一些代码,最后断言一些指定的结果出现
来看看如何运用这个概念:
// Test that `incrementTotal()` increases the total by 1
/* Arrange */
let myTestObject = new SomeObject();
/* Act */
let oldTotal = myTestObject.getTotal();
myTestObject.incrementTotal();
let newTotal = myTestObject.getTotal();
/* Assert */
if(newTotal === oldTotal + 1){
console.log("test passed!");
} else {
console.log("test failed!");
}
首先,arrange测试 -- 设置将要测试的object, 然后act — 调用object的方法, 最后 assert -- newTotal
将会等于 oldTotal+1
。
稍后,我们的测试将会更复杂一些,而且不会用 vanilla JavaScript 来写类似的代码了。但是,仍会用这个概念: Arrange, Act, Assert
One Assertion Per Test
创建测试时,在一个test中通常只有一个 assertion -- test必须测试特定的事。当然,这不是强制性的,你可以按照自己的想法轻易的在一个test中做 N多个 assertion。但是,一般来说这不是个好主意。
下面看看违反这条规则的test:
it('should allow todos to be modified', () =>{
// trigger edit todo functionality
// expect that the edit page was launched with the todo
// trigger delete todo functionality
// expect that the todo was passed to the delete function in the data provider
});
我们将要测试 todos
是否可以被更改。实际上,次数将会test两件事: todos
是否可被修改,todos
是否可被删除。为什么说这不是个好的结构嘞?:
模糊不清 --
should allow todos to be modified
这个描述太宽泛,不能一眼看出将要test的是啥玩意儿如果测试失败了,将无法知道具体是哪个断言失败 -- 是编辑失败,还是删除失败?或者两者都失败?如果分离这两个test,就会变得很明显了。
好的结构应该像下面的:
it('delete function should pass the todo to the deleteTodo method in the provider', () => {
// trigger delete function on the page
// check that the deleteTodo method on the provider has been called
});
it('edit function should launch the EditTodo page and pass in the todo as a parameter', () => {
// trigger edit function on the page
// check that the navCtrl pushed the EditTodo page with the `todo` as a parameter
});
这两个test定义的更好,而且是独立运行的。不用担心test描述搞得太长,反而越长越好,毕竟有助于你的思路完善和维护。
Make Tests Independent
通过上个示例,大概了解到tests相互独立的概念。上个例子中,由于我们在同一个test中运行了两个测试,因此,我们第一个测试将有可能改变第二个测试的行为。
及时我们没有在同一个test中跑多个测试,仍然会遇到上述问题。例如:
let groceryList = new GroceryList();
it('we should be able to push items to the grocery list', () => {
// push an item to the grocery list
// check that it was added
});
it('the grocery list should be empty by default', () => {
// check that the grocery list is empty
});
在这种情况下,第一个测试将会干扰第二个。grocery list默认为空,但是如果第一个测试添加一个item进去后就不再是默认状态了。想要解决这种问题,就需要保证在跑每个测试前 reset,例如:
let groceryList;
beforeEach(() => {
groceryList = new GroceryList();
});
it('we should be able to push items to the grocery list', () => {
// push an item to the grocery list
// check that it was added
});
it('the grocery list should be empty by default', () => {
// check that the grocery list is empty
});
我们在每个测试方法前添加了 beforeEach
方法。 这个将会在后续讨论
Isolate Unit Tests
在 one assertion per test
的例子中,我们测试了deleteTodo
方法的调用。没有测试 todo
是否真的被删除。
这种情况下,todo
的删除操作不是当前page的工作,而是 处理todos
的provider的。当前page只是把信息传给这个provider,这就是我们测试的内容。我们需要一个与provider相关的独立测试,用来测试deletion过程正确。
在一个单元测试中,我们要测试的代码需要完全的与APP的其他部分斩断关系。 不能有data传给它,不能接手server的data,不能向服务器发送data,也不能有任何其他的外部(非测试中)调用。
如果我们测试的功能依赖于server请求,或者需要有数据传递给它(比如,通过NavParams传递的参数),我们就使用 双重测试 伪造
数据。基本的想法是,如果你需要从NavParams中提取数据来执行测试,那么你实际上并不需要从NavParams中提取数据,你可以使用自己的假实现NavParams来传递测试数据。如果你需要接收响应 从服务器,你不向实际服务器发出请求,你只需要拦截请求,并使用自己的假数据进行响应。
刚开始时会比较难以掌握这些,我们想要测试APP是否工作--为什么要绕过请求服务器,而返回我们知道的正确data? 测试与其他控件、服务的集成不是单元测试的一部分,对于单元测试我们唯一需要感兴趣的是unit本身。单元测试是关于测试独立的代码块。
Don't Try to Test Everything
想象一下,我们单元测试一个方法,该方法告诉我们一个数是否是偶数。测试是这个样子:
it('isEven function should return true for even numbers', () => {
expect(isEven(4)).toBe(true);
});
it('isEven function should return false for odd numbers', () => {
expect(isEven(5)).toBe(false);
});
这两个测试可以得到预期的结果,但是这不意味着 isEven
满足所有的数字。基于这一点,可以做点儿改动让你的测试更 robust
不要这样做
it('isEven function should return true for even numbers', () => {
for(i=0; i < 2000; i+=2){
expect(isEven(i)).toBe(true);
}
});
it('isEven function should return false for odd numbers', () => {
for(i=1; i < 2000; i+=2){
expect(isEven(i)).toBe(false);
}
});
现在,我们测试的不是一个奇数或偶数了,而是1000个。这样就更好些吗?为啥不是1万个,或者一百万个?如果你测试一个用来检查通讯录中是否存在指定name的方法,我们需要弄个十万个测试name的列表,然后全部测试一遍么?
根本不可能覆盖所有的情况,也没有这个必要,你的测试也将因此变得异常耗时。一个测试数据不能证明你的代码运行正常,事实上,不可能保证测试的代码百分之百正确。
反之,我们专注于 感兴趣的
示例上。我们可以测试已知的一些偶数,还有一些已知的奇数。在检查name的示例中,可以挑选一些valid
的name,还有一些invalid
的name。
下面的示例打破了 One Assertion Per Test
的原则。你肯定不会想为同一个方法写五个独立的测试,就为了测试五个不同的值,因此,isEven
可以长这个样子:
it('isEven function should return true for even numbers', () => {
expect(isEven(0)).toBe(true);
expect(isEven(4)).toBe(true);
expect(isEven(987239874)).toBe(true);
});
一般不要在测试中用循环。
Mocks, Stubs, Spies
如前所述,已经讨论过了 隔离单元测试的伪造
功能。 它就是mocks, stubs, 和 spies的根源。
Mocks and Stubs
mock和stub基本上是同一个东西: 理解其中的区别很重要,稍后讨论。两者都会用本身实现的fake
或dummy
来替换test中的object。在此之前,我们讨论过这样的test场景:测试依赖于从不同界面提供的NavParams中获取data。我们的代码长这样:
let myValue = this.navParams.get('someValue');
如果想测试Page2
,但是NavParams提供的data来自于 Page1
,此时我们的单元测试就会出现问题,因为不能依赖其他组件的数据。
我们可以在初始时用自己伪造的实现来替换真实的 NavParams
。例如:
class myFakeNavParams {
public get(param: string): any {
return 'hello!';
}
};
这样既体现了真正 NavParams
的所有功能,同时替换了 get
方法,不管传给他什么,让它只返回 hello!
。完成了NavParams
的伪造版后,只需要在设置providers时告诉test去用伪造版。
providers: [
...
{ provide: NavParams, useClass: myFakeNavParams },
...
]
NOTE: 这一步在设置test时就完成了,稍后会讨论这个。不要在你的APP中用这个替换 真实的provider
此时,不管何时在test中引用 NavParams
,都会被我们创建的伪造版替代。如前所述, stubs和mocks 基于同一个东西:用伪造的behaviour 替换真实的 behavior。但是,有一个概念性的差别。
很难解释清楚这个差别,我不认为我脑中的概念就是100%正确的,也不认为在由什么组成mocks和由什么组成stubs上有100%的共识,所以不同太担心这玩意儿。
我能找到最好的解释就是,通常来讲,stubs用于返回测试的values。如果你伪造一个测试中需要用到的返回值,这时就需要用到 stub。意味着,上面我们讲到的示例,可以用下面的方式更合适:
spyOn(navParams, 'get').and.returnValue('hello!');
因此,不用创建一个复杂的类,我们只需要创建一个简单的stub为我们返回value就行了。但是有时,tests需要依赖于我们想要为其提供伪造实现的object的结构。假如你想创建一个loading的遮罩,此时你可以创建如下的mock:
export class LoadingControllerMock {
public create(): LoadingComponentMock {
return new LoadingComponentMock();
}
}
这不是我们要从测试到测试的改变,而是真实 LoadingController
的伪实现。这样我们可以在测试创建loading的遮罩时,不使用真正的遮罩(因为使用的话会打破隔离测试的规则)。
简言之,stubs 伪造可能在test中改变的values,而mocks伪造一个object的结构,我们不会创建相同 mock的不同版本。
Spies
Spies跟 mocks和stubs很类似,出了可以 为你spy
一些东西。一般用法如下:
spyOn(someObject, 'someMethod');
此时,不仅仅那个object被它的伪实现替换,我们还可以很容易track someMethod
方法的调用. 在test的任何一点,Spy都将会告诉我们这个方法是否被调用,调用时带的什么信息,调用了多少次,等等。
Tests Are Not Fool Proof
请记住,测试不是万无一失的。一个通过的测试并不能保证你的代码正常工作了。如果你的测试代码写的好,它将很大程度上说明你的代码做的是你想要它做的事情,但是不能百分之百保证。经常会出现测试写的不对,或者没有覆盖指定的case而导致失败。