美文网首页Ionic开发笔记
Ionic测试之自动化测试的概念与实践

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

作者: 一只特立独行的道哥 | 来源:发表于2018-09-05 15:40 被阅读64次

    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测试之自动化测试的概念与实践

        本文链接:https://www.haomeiwen.com/subject/tebawftx.html