Ionic测试之自动化测试的概念与实践

Testing Concetps

翻译于一个国外收费的ionic学习网站,原地址就不好贴出来了。

自动化测试出现了很久了,有很多关于测试的概念和最好的实践。尽管某个特定的语言会被用来描述tests,但是相同的概念可以用到任何你在用的编程语言中。

AAA: Arrange, Act, Assert

这也许是需要你理解的重点概念,是指导你创建测试的原则。创建测试代码时,有如下几个清晰简明的步骤:

  1. Arrange 布置,安排地明明白白
  2. Act 执行
  3. 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基本上是同一个东西: 理解其中的区别很重要,稍后讨论。两者都会用本身实现的fakedummy来替换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,都会被我们创建的伪造版替代。如前所述, stubsmocks 基于同一个东西:用伪造的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而导致失败。

你可能感兴趣的:(Ionic测试之自动化测试的概念与实践)