精华内容
参与话题
问答
  • 前端自动化测试

    2020-10-14 21:29:56
    所以你没有办法体会到前端自动化测试的重要性! 来说说为什么前端自动化测试如此重要! 先看看前端常见的问题: 修改某个模块功能时,其它模块也受影响,很难快速定位bug 多人开发代码越来越难以维护 不方便迭代,代码...

    目前开发大型应用,测试是一个非常重要的环节,但是大多数前端开发者对测试相关的知识是比较缺乏的,因为可能项目开发周期短根本没有机会写。所以你没有办法体会到前端自动化测试的重要性! 来说说为什么前端自动化测试如此重要! 先看看前端常见的问题:

    修改某个模块功能时,其它模块也受影响,很难快速定位bug
    多人开发代码越来越难以维护
    不方便迭代,代码无法重构
    代码质量差
    增加自动化测试后:

    我们为核心功能编写测试后可以保障项目的可靠性
    强迫开发者,编写更容易被测试的代码,提高代码质量
    编写的测试有文档的作用,方便维护
    1.测试
    1.1 黑盒测试和白盒测试
    黑盒测试一般也被称为功能测试,黑盒测试要求测试人员将程序看作一个整体,不考虑其内部结构和特性,只是按照期望验证程序是否能正常工作

    白盒测试是基于代码本身的测试,一般指对代码逻辑结构的测试。

    1.2 测试分类
    单元测试(Unit Testing) 单元测试是指对程序中最小可测试单元进行的测试,例如测试 一个函数 、 一个模块 、 一个组件 … 集成测试(Integration Testing) 将已测试过的单元测试函数进行组合集成暴露出的高层函数或类的封装,对这些函数或类进行的测试 端到端测试(E2E Testing) 打开应用程序模拟输入,检查功能以及界面是否正确

    1.3 TDD & BDD
    TDD是测试驱动开发(Test-Driven Development) TDD的原理是在开发功能代码之前,先编写单元测试用例代码 BDD是行为驱动开发(Behavior-Driven Development) 系统业务专家、开发者、测试人员一起合作,分析软件的需求,然后将这些需求写成一个个的故事。开发者负责填充这些故事的内容,保证程序实现效果与用户需求一致。 总结: TDD是先写测试在开发 (一般都是单元测试,白盒测试),而BDD则是按照用户的行为来开发,在根据用户的行为编写测试用例 (一般都是集成测试,黑盒测试)

    1.4 测试框架
    Karma Karma为前端自动化测试提供了跨浏览器测试的能力,可以在浏览器中执行测试用例
    Mocha 前端自动化测试框架,需要配合其他库一起使用,像chai、sinon…
    Jest Jest 是facebook推出的一款测试框架,集成了 Mocha,chai,jsdom,sinon等功能。

    Jest也有一些缺陷就是不能像 karam 这样直接跑早浏览器上,它采用的是 jsdom ,优势是简单、0配置! 后续我们通过jest来聊前端自动化测试

    2.Jest的核心应用
    在说 Jest 测试之前,先来看看以前我们是怎样测试的

    const parser = (str) =>{
        const obj = {};
        str.replace(/([^&=]*)=([^&=]*)/g,function(){
            obj[arguments[1]] = arguments[2];
        });
        return obj;
    }
    const stringify = (obj) =>{
        const arr = [];
        for(let key in obj){
            arr.push(`${key}=${obj[key]}`);
        }
        return arr.join('&');
    }
    // console.log(parser('name=zf')); // {name:'zf'}
    // console.log(stringify({name:'zf'})) // name=zf
    

    我们每写完一个功能,会先手动测试功能是否正常,测试后可能会将测试代码注释起来。这样会产生一系列问题,因为会污染源代码,所有的测试代码和源代码混合在一起。如果删除掉,下次测试还需要重新编写。 所以测试框架就帮我们解决了上述的问题

    2.1 前端自动化测试产生的背景及原理
    下面是最简单的测试案例

    index.js
    
    function add (a,b){
        return a+b
    }
    function minus(a,b){
        return a-b
    }
    function multi(a,b){
        return a*b
    }
    
    index.test.js
    
    function expect(result){
        return {
            toBe:function (actual){
                if(result !== actual){
                    throw new Error(`预期值和实际值不相等 预期${actual} 结果却是${result}`)
                }
            }
        }
    }
    
    function test(desc,fn){
        try {
            fn()
            console.log(`${desc} 通过测试`)
        } catch (e) {
            console.log(`${desc} 没有通过测试 ${e}`)
        }
    }
    
    test('测试加法 3+7',()=>{
        expect(add(3,7)).toBe(10)
    })
    
    test('测试减法 3-3',()=>{
        expect(minus(3,3)).toBe(0)
    })
    
    index.html
    
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>test</title>
    </head>
    <body>
        <script src="./index.js"></script>
    </body>
    </html>
    

    2.2 分组、用例
    Jest是基于模块的,我们需要将代码包装成模块的方式,分别使用 export 将 parser 、 stringify 这两个方法导出 安装 jest

    npm init -y # 初始化pacakge.json
    npm i jest
    

    我们建立一个 qs.test.js 来专门编写测试用例,这里的用例你可以认为就是一条测试功能 (后缀要以.test.js结尾,这样jest测试时默认会调用这个文件)

    import {parser,stringify} from './qs';
    it('测试 parser 是否能正常解析结果',()=>{
        // expect 断言,判断解析出来的结果是否和 {name:'zf'}相等
        expect(parser(`name=zf`)).toEqual({name:'zf'});
    })
    

    jest 默认自带断言功能,断言的意思就是判断是不是这个样子,我断定你今天没吃饭~,结果你吃了。说明这次断言就失败了,测试就无法通过 通过配置 scripts 来执行命令

    "scripts": {
        "test": "jest"
    }
    

    执行 npm run test ,可惜的是默认在 node 环境下不支持 es6模块 的语法,需要 babel 转义,当然你也可以直接使用commonjs规范来导出方法,因为大多数现在开发都采用es6模块,所以就安装一下~

    # core是babel的核心包 preset-env将es6转化成es5
    npm i @babel/core @babel/preset-env --save-dev
    

    并且配置 .babelrc 文件,告诉babel用什么来转义

    {
        "presets":[
            [
                "@babel/preset-env",{
                    "targets": {"node":"current"}
                }
            ]
        ]
    }
    

    默认jest中集成了 babel-jest ,运行时默认会调用 .babelrc 进行转义,可以直接将es6转成es5语法 运行 npm run test 出现:
    在这里插入图片描述
    继续编写第二个用例

    import {parser,stringify} from './qs';
    describe('测试qs 库',()=>{
        it('测试 parser 是否能正常解析结果',()=>{
            expect(parser(`name=zf`)).toEqual({name:'zf'});
        })
        
        it('测试 stringify 是否正常使用stringify',()=>{
            expect(stringify({name:'zf'})).toEqual(`name=zf`)
        })
    });
    

    describe的功能是给用例分组,这样可以更好的给用例分类,其实这就是我们所谓的单元测试,对某个具体函数和功能进行测试

    2.3 matchers匹配器
    在写第一个测试用例时,我们一直在使用 toEqual 其实这就是一个匹配器,那我们来看看 jest 中常用的匹配器有哪些?因为匹配器太多了,所以我就讲些常用的! 为了方便理解,我把匹配器分为三类、判断相等、不等、是否包含

    it('判断是否相等',()=>{
        expect(1+1).toBe(2); // 相等于 js中的===
        expect({name:'zf'}).toEqual({name:'zf'}); // 比较内容是否相等
        expect(true).toBeTruthy(); // 是否为 true / false 也可以用toBe(true)
        expect(false).toBeFalsy();
    });
    it('判断不相等关系',()=>{
        expect(1+1).not.toBe(3); // not取反
        expect(1+1).toBeLessThan(5); // js中的小于
        expect(1+1).toBeGreaterThan(1); // js中的大于
    });
    it('判断是否包含',()=>{
        expect('hello world').toContain('hello'); // 是否包含
        expect('hello world').toMatch(/hello/); // 正则
    });
    

    2.4 测试操作节点方法
    说了半天,我们自己来写个功能测试一下!

    export const removeNode = (node) => {
        node.parentNode.removeChild(node)
    };
    

    核心就是测试传入一个节点,这个节点是否能从 DOM 中删除

    import { removeNode } from './dom'
    it('测试删除节点',()=>{
        document.body.innerHTML = `<div><button data-btn="btn"></button</div>`
        let btn = document.querySelector('[data-btn="btn"]')
        expect(btn).not.toBeNull()
        removeNode(btn);
        btn = document.querySelector('[data-btn="btn"]');
        expect(btn).toBeNull()
    })
    

    这个就是我们所说的jsdom,在node中操作dom元素

    2.5 Jest常用命令
    我们希望每次更改测试后,自动重新执行测试,修改执行命令:

    "scripts": {
        "test": "jest --watchAll"
    }
    

    重新执行 npm run test ,这时就会监控用户的修改
    在这里插入图片描述
    提示我们按下 w
    ,显示更多信息在这里插入图片描述
    这里我把每个命令的含义都列好了,有需要可以自己尝试一下~

    3.Jest进阶使用
    1.1 异步函数的测试
    提到异步无非就两种情况,一种是回调函数的方式,一种就是现在流行的promise方式

    export const getDataThroughCallback = fn => {
      setTimeout(() => {
        fn({ name: "zf" });
      }, 1000);
    };
    export const getDataThroughPromise = () => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve({ name: "zf" });
        }, 1000);
      });
    };
    

    我们编写 async.test.js 方法

    import {getDataThroughCallback,getDataThroughPromise} from './3.getData';
    // 默认测试用例不会等待测试完成,所以增加done参数,当完成时调用done函数
    it('测试传入回调函数 获取异步返回结果',(done)=>{ // 异步测试方法可以通过done
        getDataThroughCallback((data)=>{
            expect(data).toEqual({name:'zf'});
            done();
        })
    })
    // 返回一个promise 会等待这个promise执行完成
    it('测试promise 返回结果 1',()=>{
        return getDataThroughPromise().then(data=>{
            expect(data).toEqual({name:'zf'});
        })
    })
    // 直接使用async + await语法
    it('测试promise 返回结果 2',async ()=>{
        let data = await getDataThroughPromise();
        expect(data).toEqual({name:'zf'});
    })
    // 使用自带匹配器
    it('测试promise 返回结果 3',async ()=>{
        expect(getDataThroughPromise()).resolves.toMatchObject({name:'zf'})
    })
    

    4.Jest中的mock
    4.1 模拟函数jest.fn()
    为什么要模拟函数呢?来看下面这种场景,你要如何测试

    export const myMap = (arr,fn) =>{
       return arr.map(fn)
    }
    

    打眼一看很简单啊,我只需要判断函数的返回结果就可以啦,像这样

    import { myMap } from "./map";
    it("测试 map方法", () => {
      let fn = item => item * 2;
      expect(myMap([1, 2, 3], fn)).toEqual([2, 4, 6]);
    });
    

    但是我想更细致一些,像每一次调用函数传入的是否是数组的每一项,函数是否被调用了三次,说的更明确些就是想追溯函数具体的执行过程!

    import { myMap } from "./map";
    it("测试 map 方法", () => {
      // 通过jest.fn声明的函数可以被追溯
      let fn = jest.fn(item => (item *= 2));
      expect(myMap([1, 2, 3], fn)).toEqual([2, 4, 6]);
      // 调用3次
      expect(fn.mock.calls.length).toBe(3); 
      // 每次函数返回的值是 2,4,6
      expect(fn.mock.results.map(item=>item.value)).toEqual([2,4,6])
    });
    

    详细看下这个mock中都有什么东东
    在这里插入图片描述
    4.2 模拟文件jest.mock()
    我们希望对接口进行mock,可以直接在 mocks 目录下创建同名文件,将整个文件mock掉,例如当前文件叫 api.js

    import axios from "axios";
    export const fetchUser = ()=>{
        return axios.get('/user')
    }
    export const fetchList = ()=>{
        return axios.get('/list')
    }
    

    创建 mocks/api.js

    export const fetchUser = ()=>{
        return new Promise((resolve,reject)=> resolve({user:'zf'}))
    }
    export const fetchList = ()=>{
        return new Promise((resolve,reject)=>resolve(['香蕉','苹果']))
    }
    

    开始测试

    jest.mock('./api.js'); // 使用__mocks__ 下的api.js
    import {fetchList,fetchUser} from './api'; // 引入mock的方法
    it('fetchUser测试',async ()=>{
        let data = await fetchUser();
        expect(data).toEqual({user:'zf'})
    })
    it('fetchList测试',async ()=>{
        let data = await fetchList();
        expect(data).toEqual(['香蕉','苹果'])
    })
    

    这里需要注意的是,如果mock的 api.js 方法不全,在测试时可能还需要引入原文件的方法,那么需要使用 jest.requireActual(’./api.js’) 引入真实的文件。 这里我们想这样做是不是有些麻烦呢,其实只是想将真正的请求mock掉而已,那么我们是不是可以直接 mock axios 方法呢? 在 mocks 下创建 axios.js 重写get方法

    export default {
        get(url){
            return new Promise((resolve,reject)=>{
                if(url === '/user'){
                    resolve({user:'zf'});
                }else if(url === '/list'){
                    resolve(['香蕉','苹果']);
                }
            })
        }
    }
    

    当方法中调用 axios 时默认会找 mocks/axios.js

    jest.mock('axios'); // mock axios方法
    import {fetchList,fetchUser} from './api';
    it('fetchUser测试',async ()=>{
        let data = await fetchUser();
        expect(data).toEqual({user:'zf'})
    })
    it('fetchList测试',async ()=>{
        let data = await fetchList();
        expect(data).toEqual(['香蕉','苹果'])
    })
    

    4.3 模拟时间Timer
    接着来看下个案例,我们期望传入一个callback,想看下callback能否被调用!

    export const timer = callback=>{
        setTimeout(()=>{
            callback();
        },2000)
    }
    

    因此我们很容易写出了这样的测试用例

    import {timer} from './timer';
    it('callback 是否会执行',(done)=>{
        let fn = jest.fn();
        timer(fn);
        setTimeout(()=>{
            expect(fn).toHaveBeenCalled();
            done();
        },2500)
    });
    

    有没有觉得很愚蠢,如果时间很长呢? 很多个定时器呢?这时候我们想到了 mock Timer

    import {timer} from './timer';
    jest.useFakeTimers();
    it('callback 是否会执行',()=>{
        let fn = jest.fn();
        timer(fn);
        // 运行所有定时器,如果需要测试的代码是个秒表呢?
        // jest.runAllTimers();
        
        // 将时间向后移动2.5s
        // jest.advanceTimersByTime(2500);
        // 只运行当前等待定时器
        jest.runOnlyPendingTimers();
        expect(fn).toHaveBeenCalled();
    });
    

    4.4 模拟404捕获错误及成功
    新建fetchData.js

    npm install axios --save
    
    import axios from 'axios'
    // http://www.dell-lee.com/react/api/demo.json
    const URL = 'http://www.dell-lee.com/react/api/demo1.json'
    export const fetchData = (fn) =>{
        axios.get(URL).then((Response)=>{
            fn(Response.data)
        })
    }
    

    添加 expect.assertions 来验证一定数量的断言被调用

    it('fetchData 返回结果为 404', () => {
        expect.assertions(1);
        return fetchData().catch((e) => {
            expect(e.toString().indexOf('404') > -1).toBe(true)
        })
    });
    

    resolves 捕获返回sucess

    it('fetchData 返回结果为 { success:true }', () => {
        return expect(fetchData()).resolves.toMatchObject({
            data: {
                success: true
            }
        })
    })
    

    rejects 404

    it('fetchData 返回结果为 404', () => {
        return expect(fetchData()).rejects.toThrow()
    });
    

    await

    it('fetchData 返回结果为 { success:true }', async () => {
        const response = await fetchData();
        expect(response.data).toEqual(dataCode)
    });
    

    await 404

    it('fetchData 返回结果为 { success:true }', async () => {
        expect.assertions(1);
        try {
            await fetchData();
        } catch (e) {
            expect(e.toString()).toEqual('Error: Request failed with status code 404')
        }
    });
    

    5.Jest中的钩子函数
    为了测试的便利,Jest中也提供了类似于Vue一样的钩子函数,可以在执行测试用例前或者后来执行

    class Counter {
      constructor() {
        this.count = 0;
      }
      add(count) {
        this.count += count;
      }
    }
    module.exports = Counter;
    

    我们要测试 Counter 类中 add 方法是否符合预期,来编写测试用例

    import Counter from './hook'
    it('测试  counter增加 1 功能',()=>{
        let counter = new Counter; // 每个测试用例都需要创建一个counter实例,防止相互影响
        counter.add(1);
        expect(counter.count).toBe(1)
    })
    it('测试  counter增加 2 功能',()=>{
        let counter = new Counter;
        counter.add(2);
        expect(counter.count).toBe(2)
    })
    

    我们发现每个测试用例都需要基于一个新的 counter 实例来测试,防止测试用例间的相互影响,这时候我们可以把重复的逻辑放到钩子中! 钩子函数

    beforeAll 在所有测试用例执行前执行
    afteraAll 在所有测试用例执行后
    beforeEach 在每个用例执行前
    afterEach 在每个用例执行后

    import Counter from "./hook";
    let counter = null;
    beforeAll(()=>{
        console.log('before all')
    })
    afterAll(()=>{
        console.log('after all')
    })
    beforeEach(() => {
      console.log('each')
      counter = new Counter();
    });
    afterEach(()=>{
        console.log('after');
    })
    it("测试  counter增加 1 功能", () => {
      counter.add(1);
      expect(counter.count).toBe(1);
    });
    it("测试  counter增加 2 功能", () => {
      counter.add(2);
      expect(counter.count).toBe(2);
    });
    

    钩子函数可以多次注册,一般我们通过describe 来划分作用域

    import Counter from "./hook";
    let counter = null;
    beforeAll(() => console.log("before all"));
    afterAll(() => console.log("after all"));
    beforeEach(() => {
      counter = new Counter();
    });
    describe("划分作用域", () => {
      beforeAll(() => console.log("inner before")); // 这里注册的钩子只对当前describe下的测试用例生效
      afterAll(() => console.log("inner after"));
      it("测试  counter增加 1 功能", () => {
        counter.add(1);
        expect(counter.count).toBe(1);
      });
    });
    it("测试  counter增加 2 功能", () => {
      counter.add(2);
      expect(counter.count).toBe(2);
    });
    // before all => inner before=> inner after => after all
    // 执行顺序很像洋葱模型 ^-^
    

    6.Jest中的配置文件
    我们可以通过jest命令生成jest的配置文件

    npx jest --init
    

    会提示我们选择配置项:

    ➜  unit npx jest --init
    The following questions will help Jest to create a suitable configuration for your project
    # 使用jsdon
    ✔ Choose the test environment that will be used for testing › jsdom (browser-like)
    # 添加覆盖率
    ✔ Do you want Jest to add coverage reports? … yes
    # 每次运行测试时会清除所有的mock
    ✔ Automatically clear mock calls and instances between every test? … yes
    

    在当前目录下会产生一个 jest.config.js 的配置文件

    7.Jest覆盖率
    刚才产生的配置文件我们已经勾选需要产生覆盖率报表,所有在运行时我们可以直接增加 --coverage 参数

    "scripts": {
        "test": "jest --coverage"
    }
    

    可以直接执行 npm run test ,此时我们当前项目下就会产生coverage报表来查看当前项目的覆盖率

    ---------|----------|----------|----------|----------|-------------------|
    File      |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
    ----------|----------|----------|----------|----------|-------------------|
    All files |      100 |      100 |      100 |      100 |                   |
     hook.js  |      100 |      100 |      100 |      100 |                   |
    ----------|----------|----------|----------|----------|-------------------|
    Test Suites: 1 passed, 1 total
    Tests:       2 passed, 2 total
    Snapshots:   0 total
    Time:        1.856s, estimated 2s
    

    命令行下也会有报表的提示,jest增加覆盖率还是非常方便的~

    Stmts表示语句的覆盖率
    Branch表示分支的覆盖率(if、else)
    Funcs函数的覆盖率
    Lines代码行数的覆盖率
    到此我们的 Jest 常见的使用已经基本差不多了!接下我们来看看如何利用Jest来测试Vue项目!

    8.Vue中集成Jest
    我们可以通过 vue 官方提供的 @vue/cli 直接创建Vue项目,在创建前需要先安装好@vue/cli~ 这里直接创建项目:

    vue create vue-unit-project
    
    ? Please pick a preset:
      default (babel, eslint)
    ❯ Manually select features # 手动选择
    
    ? Check the features needed for your project:
     ◉ Babel
     ◯ TypeScript
     ◯ Progressive Web App (PWA) Support
     ◉ Router
     ◉ Vuex
     ◯ CSS Pre-processors
     ◯ Linter / Formatter
    ❯◉ Unit Testing
     ◯ E2E Testing
    
    ? Please pick a preset: Manually select features
    ? Check the features needed for your project: Babel, Router, Vuex, Unit
    ? Use history mode for router?  # history模式
    ion) Yes
    ? Pick a unit testing solution: Jest # 测试框架选择Jest
    ? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? In dedicated config  # 将配置文件产生独立的文件
     files
    ? Save this as a preset for future projects? (y/N) # 是否保存配置
    

    初始化成功后,我们先来查看项目文件,因为我们主要关注的是测试,所以先来查看下 jest.config.js 文件

    module.exports = {
      moduleFileExtensions: [ // 测试的文件类型
        'js','jsx','json','vue'
      ],
      transform: { // 转化方式
        '^.+\\.vue$': 'vue-jest', // 如果是vue文件使用vue-jest解析
        '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub', // 如果是图片样式则使用 jest-transform-stub
        '^.+\\.jsx?$': 'babel-jest' // 如果是jsx文件使用 babel-jest
      },
      transformIgnorePatterns: [ // 转化时忽略 node_modules
        '/node_modules/'
      ],
      moduleNameMapper: { // @符号 表示当前项目下的src
        '^@/(.*)$': '<rootDir>/src/$1'
      },
      snapshotSerializers: [ // 快照的配置
        'jest-serializer-vue'
      ],
      testMatch: [ // 默认测试 /test/unit中包含.spec的文件 和__tests__目录下的文件
        '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
      ],
      testURL: 'http://localhost/', // 测试地址
      watchPlugins: [ // watch提示插件
        'jest-watch-typeahead/filename',
        'jest-watch-typeahead/testname'
      ]
    }
    

    通过配置文件的查看我们知道了所有测试都应该放在 tests/unit 目录下! 我们可以查看 pacakge.json 来执行对应的测试命令

    "scripts": {
        "serve": "vue-cli-service serve",
        "build": "vue-cli-service build",
        "test:unit": "vue-cli-service test:unit --watch" // 这里增加个 --watch参数
    },
    

    开始测试 npm run test:unit

    9.测试Vue组件
    我们先忽略默认 example.spec.js 文件,先来自己尝试下如何测试 Vue组件

    9.1 测试HelloWorld组件

    <template>
      <div class="hello">
        <h1>{{ msg }}</h1>
      </div>
    </template>
    <script>
    export default {
      name: 'HelloWorld',
      props: {
        msg: String
      }
    }
    </script>
    

    HelloWorld 组件需要提供一个msg属性,将msg属性渲染到 h1 标签中,ok我们来编写测试用例 在 tests/unit 下创建 HelloWorld.spec.js

    import Vue from 'vue';
    import HelloWorld from '@/components/HelloWorld'
    describe('测试HelloWolrd 组件',()=>{
        it('传入 msg 属性看能否渲染到h1标签内',()=>{
            const  baseExtend = Vue.extend(HelloWorld);
            // 获取当前组件的构造函数,并且挂载此组件
            const vm = new baseExtend({
                propsData:{
                    msg:'hello'
                }
            }).$mount();
            expect(vm.$el.innerHTML).toContain('hello');
        })
    });
    

    这样一个简单的Vue组件就测试成功了,但是写起来感觉不简洁也不方便!所以为了更方便的测试Vue官方提供给我们了个测试工具 Vue Test Utils ,而且这个工具为了方便应用,采用了同步的更新策略

    import Vue from 'vue';
    import HelloWorld from '@/components/HelloWorld';
    import {shallowMount} from '@vue/test-utils'
    describe('测试HelloWolrd 组件',()=>{
        it('传入 msg 属性看能否渲染到h1标签内',()=>{
            const wrapper = shallowMount(HelloWorld,{
                propsData:{
                    msg:'hello'
                }
            })
            expect(wrapper.find('h1').text()).toContain('hello')
        });
    });
    

    这样写测试是不是很hi,可以直接渲染组件传入属性,默认返回 wrapper , wrapper 上提供了一系列方法,可以快速的获取dom元素! 其实这个测试库的核心也是在 wrapper 的方法上, 更多方法请看 Vue Test Utils 这里的 shallowMount 被译为潜渲染,也就是说 HelloWorld 中引入其他组件是会被忽略掉的,当然也有深度渲染 mount 方法! 刚才写测试的这种方式就是 先编写功能 !编写完成后,我们来 模拟用户的行为进行测试 ,而且只测试其中的某个具体的功能! 这就是我们所谓的 BDD形式的单元测试 。接下来,我们再来换种思路再来写个组件!

    9.2 测试Todo组件
    这回呢,我们来采用 TDD的方式 来测试,也就是 先编写测试用例 先指定测试的功能: 我们要编写个Todo组件

    当输入框输入内容时会将数据映射到组件实例上
    如果输入框为空则不能添加,不为空则新增一条
    增加的数据内容为刚才输入的内容
    编写 Todo.spec.js

    import Todo from '@/components/Todo.vue';
    import {shallowMount} from '@vue/test-utils'
    describe('测试Todo组件',()=>{
        it('当输入框输入内容时会将数据映射到组件实例上',()=>{
            // 1) 渲染Todo组件
            let wrapper = shallowMount(Todo);
            let input = wrapper.find('input');
            // 2.设置value属性 并触发input事件
            input.setValue('hello world');
            // 3.看下数据是否被正确替换
            expect(wrapper.vm.value).toBe('hello world')
        });
        it('如果输入框为空则不能添加,不为空则新增一条',()=>{
            let wrapper = shallowMount(Todo);
            let button = wrapper.find('button');
            // 点击按钮新增一条
            wrapper.setData({value:''});// 设置数据为空
            button.trigger('click');
            expect(wrapper.findAll('li').length).toBe(0);
            wrapper.setData({value:'hello'});// 写入内容
            button.trigger('click');
            expect(wrapper.findAll('li').length).toBe(1);
        });
        it('增加的数据内容为刚才输入的内容',()=>{
            let wrapper = shallowMount(Todo);
            let input = wrapper.find('input');
            let button = wrapper.find('button');
            input.setValue('hello world');
            button.trigger('click');
            expect(wrapper.find('li').text()).toMatch(/hello world/);
        });
    });
    

    我们为了跑通这些测试用例,只能被迫写出对应的代码!

    <template>
     <div>
      <input type="text" v-model="value" />
      <button @click="addTodo"></button>
      <ul>
       <li v-for="(todo,index) in todos" :key="index">{{todo}}</li>
      </ul>
     </div>
    </template>
    <script>
    export default {
     methods: {
      addTodo() {
       this.value && this.todos.push(this.value)
      }
     },
     data() {
      return {
       value: "",
       todos: []
      };
     }
    };
    </script>
    

    以上就是我们针对Todo这个组件进行了单元测试,但是真实的场景中可能会更加复杂,在真实的开发中,我们可能将这个 Todo 组件进行拆分,拆分成 TodoInput 组件和 TodoList 组件和 TodoItem 组件,如果采用单元测试的方式,就需要依次测试每个组件( 单元测试是以最小单元来测试 ) 但是单元测试无法保证整个流程是可以跑通的,所以我们在单元测试的基础上还要采用 集成测试 总结: 1.单元测试可以保证测试覆盖率高,但是相对测试代码量大,缺点是无法保证功能正常运行 2.集成测试粒度大,普遍覆盖率低,但是可以保证测试过的功能正常运行 3.一般业务逻辑会采用BDD方式使用集成测试(像测试某个组件的功能是否符合预期)一般工具方法会采用TDD的方式使用单元测试 4.对于 UI 组件来说,我们不推荐一味追求行级覆盖率,因为它会导致我们过分关注组件的内部实现细节,从而导致琐碎的测试

    9.3 测试Vue中的异步逻辑
    在测试Vue项目中,我们可能会在组件中发送请求,这时我们仍然需要对请求进行mock

    <template>
      <ul>
       <li v-for="(list,index) in lists" :key="index">{{list}}</li>
      </ul>
    </template>
    <script>
    import axios from 'axios'
    export default {
     async mounted(){
        let {data} = await axios.get('/list');
        this.lists = data;
     },
     data() {
      return {
       lists: []
      };
     }
    };
    </script>
    

    如何实现 jest 进行方法的 mock

    import List from "@/components/List.vue";
    import { shallowMount } from "@vue/test-utils";
    jest.mock("axios");
    it("测试List组件", done => {
      let wrapper = shallowMount(List);
      setTimeout(() => {
        expect(wrapper.findAll("li").length).toBe(3);
        done();
      });
    });
    

    这里使用setTimeout的原因是我们自己mock的方法是promise,所以是微任务,我们期望微任务执行后在进行断言,所以采用setTimeout进行包裹,保证微任务已经执行完毕! 如果组件中使用的不是 async、await 形式,也可以使用 $nextTick , (新版node中 await 后的代码会延迟到下一轮微任务执行)

    举个例子:

    function fn(){
        return new Promise((resolve,reject)=>{
            resolve([1,2,3]);
        })
    }
    async function getData(){
        await fn(); 
        // await fn()  会编译成
        // new Promise((resolve)=>resolve(fn())).then(()=>{
        //     console.log(1)
        // })
        console.log(1);
    }
    getData();
    Promise.resolve().then(data=>{
        console.log(2);
    });
    

    当然不同版本执行效果可能会有差异

    来简单看下不是 async、await 的写法~~~

    axios.get('/list').then(res=>{
        this.lists = res.data;
    })
    
    it('测试List组件',()=>{
        let wrapper = shallowMount(List);
        // nextTick方法会返回一个promise,因为微任务是先进先出,所以nextTick之后的内容,会在数据获取之后执行
        return wrapper.vm.$nextTick().then(()=>{
            expect(wrapper.vm.lists).toEqual([1,2,3])
        })
    })
    

    9.4 测试Vue中的自定义事件
    我们写了一个切换显示隐藏的组件,当子组件触发change事件时可以切换p标签的显示和隐藏效果

    <template>
        <div>
            <Head @change="change"></Head>
            <p v-if="visible">这是现实的内容</p>
        </div>
    </template>
    <script>
    import Head from './Head'
    export default {
        methods:{
            change(){
                this.visible = !this.visible;
            }
        },
        data(){
            return {visible:false}
        },
        components:{
            Head
        }
    }
    </script>
    

    我们来测试它!可以直接通过 wrapper.find 方法找到对应的组件来发射事件

    import Modal from '@/components/Modal';
    import Head from '@/components/Head';
    import {mount, shallowMount} from '@vue/test-utils'
    it('测试 触发change事件后 p标签是否可以切换显示',()=>{
        let wrapper = shallowMount(Modal);
        let childWrapper = wrapper.find(Head);
        expect(wrapper.find('p').exists()).toBeFalsy()
        childWrapper.vm.$emit('change');
        expect(childWrapper.emitted().change).toBeTruthy(); // 检验方法是否被触发
        expect(wrapper.find('p').exists()).toBeTruthy(); // 检验p标签是否显示
    })
    

    到这里我们对 vue 的组件测试已经基本搞定了,接下来我们再来看下如何对Vue中的 Vuex 、 Vue-router 进行处理

    10.测试时使用VueRouter
    10.1 存根
    在你的组件中引用了全局组件 router-link 或者 router-view 组件时,我们使用 shallowMount 来渲染会提示无法找到这两个组件,我们可以使用存根的方式 mock 掉相关的组件,

    <template>
        <div>
            <h1>当前路由:{{this.$route.path}}</h1>
            <router-link to="/">首页</router-link>
            <router-link to="/about">关于页面</router-link>
            <router-view></router-view>
        </div>
    </template>
    
    import Nav from "@/components/Nav.vue";
    import { shallowMount } from "@vue/test-utils";
    it("测试Nav组件", () => {
      let wrapper = shallowMount(Nav,{
          // 忽略这两个组件
          stubs:['router-link','router-view'],
          mocks:{ // mock一些数据传入到Nav组件中
            $route:{path:'/'}
          }
      });
      expect(wrapper.find('h1').text()).toContain('/')
    });
    

    同理:我们可以mock掉一些全局组件,也可以mock一些参数传入到组件中。

    1 0.2 安装VueRouter
    我们可以也创建一个 localVue 来安装VueRouter,传入到组件中进行渲染。 安装 Vue Router 之后 Vue 的原型上会增加 $route 和 $router 这两个只读属性。所以不要挂载到基本的Vue构造函数上,同时也不能通过 mocks 参数重写这两个属性

    const localVue = createLocalVue();
    localVue.use(VueRouter);
    it("测试Nav组件", () => {
        let router = new VueRouter({
            routes:[
              {path:'/',component:Home},
              {path:'/about',component:About}
            ]
        });
        let wrapper = mount(Nav,{
            localVue,
            router
        });
        router.push('/about');
        expect(wrapper.find('h1').text()).toMatch(/about/)
    });
    

    11.Vuex的测试
    我们通过一个计数器的例子来掌握如何测试vuex

    <template>
        <div>
            {{this.$store.state.number}}
            <button @click="add(3)">添加</button>
        </div>
    </template>
    <script>
    import {mapActions} from 'vuex';
    export default {
        methods:{
            ...mapActions({'add':'increment'})
        }
    }
    </script>
    

    编写 store/index.js

    import Vue from 'vue'
    import Vuex from 'vuex'
    import config from './config'
    Vue.use(Vuex)
    export default new Vuex.Store(config)
    

    编写 store/mutations.js

    export default {
        increment(state,count){
            state.number+=count
        }
    }
    

    编写 store/actions.js

    export default {
      increment({ commit }, count) {
        setTimeout(() => {
          commit("increment", count);
        }, 1000);
      }
    };
    

    编写 store/config.js

    import mutations from "./mutations";
    import actions from "./actions";
    export default {
      state: {
        number: 0
      },
      mutations,
      actions
    };
    

    这里我们就不过多的详细讲解vuex的执行过程了,直接开始测试啦!

    11.1 单元化测试store
    我们可以直接把store中的方法一一进行单元测试 就是一个个测试函数,但是需要mock commit 和 dispatch 方法

    import mutations from '../mutations';
    import actions from '../actions';
    jest.useFakeTimers();
    it('测试mutation',()=>{
        const state = {number:0}
        mutations.increment(state,2);
        expect(state.number).toBe(2);
    });
    it('测试action',()=>{
        let commit = jest.fn();
        actions.increment({commit},2);
        jest.advanceTimersByTime(2000);
        expect(commit).toBeCalled();
        expect(commit.mock.calls[0][1]).toBe(2);
    });
    

    11.2 测试运行的store
    就是产生一个store,进行测试 好处是不需要 mock 任何方法

    import Vuex from 'vuex';
    import {createLocalVue} from '@vue/test-utils'
    import config from '../config';
    jest.useFakeTimers();
    it('测试是否可以异步增加 1',()=>{
        let localVue = createLocalVue();
        localVue.use(Vuex);
        let store = new Vuex.Store(config); // 创建一个运行store
        expect(store.state.number).toBe(0);
        store.dispatch('increment',2);
        jest.advanceTimersByTime(2000); // 前进2s
        expect(store.state.number).toBe(2); 
    });
    

    config文件最好每次测试时克隆一份,保证每个用例间互不干扰!

    11.3 测试组件中的Vuex
    mock store 传入组件中,看函数是否能够如期调用

    import Vuex from 'vuex';
    import Counter from '@/components/Counter';
    import {createLocalVue,shallowMount} from '@vue/test-utils'
    let localVue = createLocalVue();
    localVue.use(Vuex);
    let store;
    let actions;
    beforeEach(()=>{
        actions = {
            increment:jest.fn()
        }
        store = new Vuex.Store({
            actions,
            state:{}
        });
    });
    it('测试组件中点击按钮 是否可以 1',()=>{
        let wrapper = shallowMount(Counter,{
            localVue,
            store
        });
        wrapper.find('button').trigger('click');
        // 测试actions中的increment 方法是否能正常调用
        expect(actions.increment).toBeCalled();
    })
    

    到这里 Vuex 测试的方式我们就讲解完毕了, 其实前端自动化测试并不难~,大家多多练习就可以完全掌握啦!

    展开全文
  • 一起来了解下前端自动化测试吧,让测试更有效率,代码质量更有保障。 课程大纲 一.自动化测试基础知识 为什么写测试 测试分层 - 测试金字塔 两种测试风格 Mock 分类 测试要素、关注点 二.实战 测试框架选型 简单...
  • 前端自动化测试实践

    万次阅读 多人点赞 2018-10-31 23:35:49
    通过前端自动化测试,来解放自我

    前言
            我本身就职于某安防企业,正儿八经的传统企业,这两年还有个别称叫“大白马”,懂的人自然懂。做的产品说大一点和物联网沾边吧。服务的客户主要还是以大中型国企和政府机关,所以很多产品的使用群体没有互联网公司来的那么庞大,因此也造成了我们之前许多产品在设计和开发时重功能,抢交付,轻体验的坏毛病,不止是我们公司,我了解到的大多数同行业公司都有相同的毛病。但是互联网飞速发展的今天,这些以前留下来的毛病越来越被客户诟病,用户体验已经成为了产品核心竞争力的一部分。这些年来公司在这一点上也在积极的改变,伴随而来的就是测试越是越来严苛,现在我们有时候看见测试的妹子都是绕着走。这可能也是某种程度上造成我们现在团队单身比例比较高的原因吧<<^^>>。对于我们前端来说工作量就更加重了,每天上午被测试妹子各种打扰,下午参见各种需求分析和UI/UE评审,然后晚上开始写代码,好不容易代码写完了,本以为告一段罗了,可以有时间处理一下个人的问题了。哎!想多了,发测阶段才是噩梦的开始,有时候我们自己也想不通,为什么一到发测的时候各种问题就开始出现了,然后就是各种加班,大家感觉很累,但是又没什么成就感,就是重复的的修改,重复的自测,测得你怀疑人生。有时候真的好羡慕那些搞后端的,他们分分钟就知道程序有没有什么问题,而我们要一遍一遍的去操作页面,填写数据,提交数据,查看页面显示是否异常,哎…都是泪。可能说到这里有很多搞前端的兄弟已经生有同感了。于是,我也开始了走上了自动化测试的道路。

    目录:

    1. 作为一个前端开发人员自动化测试我心中的疑问
    2. 什么让我又坚定不移的走上了自动化测试的道路
    3. 如何进行前端自动化测试

    一 关于前端自动化测试最开始存在的疑问

         其实关于自动化测试,我好早之前就了解过,由于自己写了一段时间的java,后来开始做前端开发,关于前端自动化测试我心中有一直有各种疑问,另一方面工作压力的激增,所以就没有真正的去了解和实践。

         关于疑问主要只有下面七点:

    1. 前端页面是整个应用的入口,他直接面向客户的,他更多时候体现的是与客户的互动和良好的有户体验,这和后端开发是有本质上的区别,后端更加关注的是数据,如何快速,安全的提供数据,存储数据。特别是RESTful风格盛行的当下,前后端的分工更加明确,后端的自动化测试相对以前没有分离时变得更加容易,更多的时候他们只需要保证他们提供给前端的接口安全可靠就行了。
    2. 前端自动化测试需要测试的方面太多, 除了常见的单元测试,前端还需要对UI组件和页面进行测试,来判断输出页面是否达到了交互稿的效果,这一点也是目前前端测试中比较复杂的,一方面现在前端页面展复杂多变,现另外一方面很多时候对输出的页面已经精确到了像素级别了,这你让我们怎么测,好像貌似没有比肉眼更快速,靠谱了的方式了,那这部分工作能不能用程来解决呢?
    3. 需要模拟大量的交互事件,前面我们提到前端做的更多是与用户进行互动,那么自动化测试的时候就需要模拟大量的用户行为。PC端我们需要用代码模拟click,dbclick,mousedown,mouseup,mousemove,drag,input…,移动端我们还需要模拟touch,scroll,gesture…,关键是怎么模拟?
    4. 运行前端的终端设备种类太多,PC,Mobile Phone,Pad…这些意味着我们需要去适配不同的终端,那么在测试的时候我们是不是也需模拟在不同的终端进行测试呢,终端模拟完了是不是还要模拟在不同浏览器下加载我们的应用呢?哎…老瓜疼,老瓜疼。
    5. 性能测试,纳尼?前端还有性能测试,前端不就是将将设计稿和交互稿通过代码实现页面输出就好了吗?怎么还有性能测试?别紧张,所谓性能不过就是页面的加载快慢,白屏时间,页面交互的流畅性。哦!这个自己打开浏览浏览网页,自己体验一下不就好了嘛,实在不行打开F12调试窗口,资源加载快慢一目了然,还要什么自动化测试?怎么自动化测试?自动化测试又能带来什么好处?

          一口气说了5点,完了吗?没有,我前面提到了自己就职于传统行业,那么“传统”二字怎么体现出来呢?接着说

    1. 除了常规的前端技术外,像我们的公司的前端里面业务逻辑重还夹杂着类似OCX控件,NP插件…等等非常规浏览器对象的操作,这部分需要和C++进行交互。那这又怎么测试?
    2. 传统IT企业团队选择技术相对保守,一方面是行业限制,因为有的客户就是要求你的产品必须在IE6上运行,你能怎么着,vue,react…这些现在被吹上天的前端技术你用的着吗?没用。另外一个问题就是开发人员的业务能力,很多这类企业由于自身所处业务的限制,所以在招人的时候,可能更多的也是看学历,当然长相也可以看看,至于业务能力,这些都是可以慢慢培养的嘛!但是,就公司这种传统的技术氛围又能培养出怎么的开发人员呢?大多数人也就是围绕着这些传统的前端技术转圈,一圈又一圈。但是有一点可以肯定,那就是加班的习惯肯定给你培养出来。好多工作一两年的前端开发人员人都只是听过前端自动化测试,但是基本都没有什么实际经验,因此在自动化测试方面,就只有自己去摸索了,完了之后团队还不一定会用这套测试方案,因为交付时间不允许。
    3. 好了,说了这么多,累了,直接跳到最大的问题,那就是时间,不管是传统企业还是互联网大厂,小厂,时间对于开发人员来说永远是不够的,这么短的开发周期,要做这么多的事,开发工作都干不完,哪还有什么时间和精力写自动化测试脚本,能把功能交付了就哦弥陀佛了,至于测试那还是较给测试部门吧。

          综上所有因素,结果就是一个,罢了,务实一点,还是加班手动测吧。

    二 什么让我又坚定不移的走上了自动化测试的道路?

          前面讲了这么多前端实现自动化测试的难点,但是我还是依然觉决定去深入学习前端自动化测试,并积极的在团队中推广。究其原因,总结一下:
            首先,我个人认为无论前端后端,只要是用代码写的,都需要进行自动化测试,测试的全不全我们暂且不说,测试需要伴随整个的开发过程,不能全部将发现问题的时间堆到测试部门介入后,这样一来产品发测的风险会很大,有可能会被打回来,严重影响产品发布。二来,就像我像前面提到的那样,可能发测前各种问题会蜂拥而至,造成自己天天加班,熬夜多了,你懂的。自动化测试可以帮我们提前暴露问题,节约我们手动跑测试用例的时间。
          其次,目前市面上前端自动化测试的方案已经比较成熟了,我们前面提到的那些问题,大部分是可以得到较好解决的,社区里面前端达人们分享了许多关于前端自动化测试的经验。所以在前端自动化测试,在技术上是没有问题的,所以我们需要大胆的去尝试。
            再者,这是前端发展的趋势,我们可以打开目前开源的热门框架,Vue,React,UI组件库iviewelement-ui,以及常用的npm包的源代码看看,他们的目录结构里面绝对都含有自动化测试脚本,可见前端自动化测试不仅仅是花钱秀腿。历史的车轮是滚滚向前的,谁都停不下来,身为前端的我们也不能再是一个切图仔,页面小王子了。互联网技术的发展,用户对于产品的更高要求,产品的快速迭代,这些都要求我们前端开发人员需要具备更高的开发效率,而自动化测试貌似是我们加班之外最好的一个选择了。
          最重要的一点,每次的发测太折磨了。

    三 如何进行前端自动化测试?

    首先,先了解一下前端自动化测试的分类

    1. 单元测试
    2. e2e测试
    3. 视觉回归测试
    4. 性能测试

    单元测试

            对程序中某一块独立的业务模块进行测试,可以是一个小功能,也一个函数,这属于白盒测试。下面是单元测试常用的组合方案:
    1. karma+Jasmine+PhantomJS
    2. karma+mocha+PhantomJS
         &nbsp首先两者没有什么本质的区别,mocha相对于Jasmine出来的要晚一点,可以使用的多种断言库,包括nodejs的assert断言。
            我自己更习惯使用mocha,两种写法很相像的,切换起来也是比较容易的。

    安装:

    npm install karma-cli -g
    

    利用工具自动生成测试配置项

    karma init
    

    然后通过键盘的方向建,现在测试框架的组合

    PS E:\workpace\units> karma init
    
    Which testing framework do you want to use ?
    Press tab to list possible options. Enter to move to the next question.
    > mocha
    
    Do you want to use Require.js ?
    This will add Require.js plugin.
    Press tab to list possible options. Enter to move to the next question.
    > yes
    
    Do you want to capture any browsers automatically ?
    Press tab to list possible options. Enter empty string to move to the next question.
    > PhantomJS
    >
    
    What is the location of your source and test files ?
    You can use glob patterns, eg. "js/*.js" or "test/**/*Spec.js".
    Enter empty string to move to the next question.
    >
    
    Should any of the files included by the previous patterns be excluded ?
    You can use glob patterns, eg. "**/*.swp".
    Enter empty string to move to the next question.
    >
    
    Do you wanna generate a bootstrap file for RequireJS?
    This will generate test-main.js/coffee that configures RequireJS and starts the tests.
    > no
    
    Which files do you want to include with <script> tag ?
    This should be a script that bootstraps your test by configuring Require.js and kicking __karma__.start(), probably your test-main.js file.
    Enter empty string to move to the next question.
    >
    
    Do you want Karma to watch all the files and run the tests on change ?
    Press tab to list possible options.
    > yes
    
    
    Config file generated at "E:\workpace\units\karma.conf.js".
    
    npm install karma chai requirejs karma-mocha karma-requirejs --save-dev
    

    经过上面的步骤就可以开始写测试案例了。

    我在单元测试中测试的有:

    1. 复杂的业务模块处理函数
    //person.js
    class Person{
      constructor(name,age){
         this.name = name;
         this.age = age;
      }
      /*
       * 个人介绍
      */
      introduce(){
        return `大家好,我是${this.name},今年${this.age}岁,我是一名前端开发工程师.` 
      }
      /*
       * 请求个人发表的文章列表
      */
      getArticleList(){
            return new Promise((resolve,reject)=>{
                 setTimeout(()=>{
                     const list = {
                         code:200,
                         data:[{
                            id:'001',
                            name:'前端自动化测试的实践之路',
                         }],
                        desc:'请求成功'
                     }
                     resolve(list)
                 },1000)
            })
      }
    }
    module.exports = Person;
    

          下面开始写测试脚本,一般一个模块写一个测试脚本文件,比如要测前面的Person类,就在test文件中创建person.spec.js测试脚本文件

    //person.spec.js
    const person = require("../src/person.js");
    const expect = require("chai").expect;
    describe('person.js', () => {
      it('1.测试个人介绍函数introduce', () => {
        const name = "王洋洋",age = 26;
        const Person = new person(name,age);
        expect(Person.introduce()).to.be.equal(`大家好,我是${name},今年${age}岁,我是一名前端开发工程师.` )
      })
    })
    
    1. 测试异步请求

          这里我一般用来测试与后台数据交互的接口,现在的web基本上都是前后端分离的开发模式,前后端的数据基都是通过异步请求的方式获取到的,我们在自动化测试的时候有必要模拟所有参数然后对所有的CGI进行测试。这里接着在上面的person.spec.js中的person.js测试套件中写用例,顺便提一下describe定义的就是一个测试套件,it定义的是一个测试用例,一个套件中可以包含多个其他的测试套件或测试用例。下面模拟一个异步请求的测试用例 :

     it('1.测试个人介绍函数introduce', (done) => {
        const name = "王洋洋",age = 26;
        const Person = new person(name,age);
        Person.getArticleList().then(result=>{
            expect(result).to.be.an('object');
            done()
        })
     })
    

    执行启动测试的命令

    PS E:\workpace\units> mocha ./test/person.spec.js
    
    
      person.js
        √ 1.测试个人介绍函数introduce
        √ 1.测试请求个人文章列表的异步请求 (1004ms)
    
    
      2 passing (1s)
    
    PS E:\workpace\units>
    

          可以看到异步请求的测试结果中还顺带有请求响应的实践,这个对后面的性能测试,也是有用 的。当然,真是的业务功能逻辑不可能这样简单,这里只是提到了这两中情况怎么测,具体到真实的业务场景中需要根据具体的业务功能写更加复杂的测试案例。

    1. 测试封装的UI组件

          上面两种测试用例是对js代码块的测试,目前前端提倡组件化,模块化,像什么Vue,React框架在各大公司盛行,我本身就是一个Vue框架的深度用户。这些框架衍生出来了各种UI组件库,比如Vue的iviewelement-ui,React的uxcore…我们打开这些组件的npm包,我们发现他们的目录下面都有一个test文件夹,这里面有关于各个组件的各种测试案例。这里就是我们提到的对于封装的UI组件的单元测试,这个是很有必要的,因为在平时的工作中,我们也会写一些行业内的公用组件来进行重复利用的,这个组件也会在后续不断的迭代,如果没有自动化测试,每次迭代都需要手动把所有功能重新测试一下,想像一下,这需要多少时间,如果有自动化测试,5s中不到可能就OK了。好了,下面我们写一个Vue组件的测试案例,至于为什么是Vue,因为我React不熟…。万事万物都事相同的。其实vue官方脚手架中已经将自动化测试的选项加进入了,我们只需要在初始化项目目录的时候选择就行了

    vue init webpack unit-demo
    
    ? Project name unit-demo
    ? Project description A Vue.js project
    ? Author wangyy <wang839305939@outlook.com>
    ? Vue build standalone
    ? Install vue-router? Yes
    ? Use ESLint to lint your code? No
    ? Set up unit tests Yes
    ? Pick a test runner karma
    ? Setup e2e tests with Nightwatch? Yes
    ? Should we run `npm install` for you after the project has been created? (recommended) npm
    
       vue-cli · Generated "unit-demo".
    
    
    # Installing project dependencies ...
    # ========================
    

    这里与有可能安装依赖失败,如果失败可以重新用cnpm装一次

    cnpm install
    //安装结束后执行单元测试命令
    npm run unit
    
    unit-demo@1.0.0 unit E:\workpace\units\vuetest\unit-demo
    > cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run
    
    30 10 2018 18:26:26.142:INFO [karma]: Karma v1.7.1 server started at http://0.0.0.0:9876/
    30 10 2018 18:26:26.159:INFO [launcher]: Launching browser PhantomJS with unlimited concurrency
    30 10 2018 18:26:26.173:INFO [launcher]: Starting browser PhantomJS
    30 10 2018 18:26:38.168:INFO [PhantomJS 2.1.1 (Windows 8.0.0)]: Connected on socket M5J-odoqXKpQY1WYAAAA with id 51250636
    
      HelloWorld.vue
        √ should render correct contents
    
    PhantomJS 2.1.1 (Windows 8.0.0): Executed 1 of 1 SUCCESS (0.063 secs / 0.03 secs)
    TOTAL: 1 SUCCESS
    
    
    =============================== Coverage summary ===============================
    Statements   : 100% ( 2/2 )
    Branches     : 100% ( 0/0 )
    Functions    : 100% ( 0/0 )
    Lines        : 100% ( 2/2 )
    ================================================================================
    PS E:\workpace\units\vuetest\unit-demo>
    

    hello.vue组件测试用例

    import Vue from 'vue'
    import Hello from '@/components/Hello'
    
    describe('Hello.vue', () => {
      it('should render correct contents', () => {
        const Constructor = Vue.extend(Hello)
        const vm = new Constructor().$mount()
        expect(vm.$el.querySelector('.hello h1').textContent)
          .to.equal('Welcome to Your Vue.js App')
      })
    })
    

    通过用例可以发现,Vue UI组件的测试关键点就是通过创建出dom元素,然后去检测dom元素中的特征值,然后判断该测试用例是否通过,这只是一个思路,具体要测试那些特性,还是要根据组件的复杂度来写案例。

    e2e测试

          也叫端到端测试,从测试目的上说它是用来测试一个应用从头到尾的流程是否和设计时候所想的一样。简而言之,它是从用户的角度出发,认为整个系统都是一个黑盒,只有UI会暴露给用户。从测试实现上来说就是以功能来作为最小测试单元,通过测试脚本模拟用户的行为然后驱动浏览器来自动测试这些功能,比如自登陆,退出,页面跳转等一系列用户和界面的交互的功能。这一点最开始我是处于懵逼状态,因为大多数情况下都是人为的去测试功能点,至于通过测试脚本去驱动浏览器。即使偶尔异想天开想过,也无从下手。当然,自己不知道,不代表别人不知道,当我们打开自动化测试的大门后,你会发现里面好多大牛在这方面已经有很深的耕耘了。我们需要做的就是沿着他们的研究轨迹,不断的去丰富我们在这方面的知识,如果有一天自己有什么想法也可以积极的探索,积极的和大家分享。不多说,还是从vue-cli创建的工程来了解e2e测试
          在初始化工程好后,直接运行

    npm run e2e
    

          开始执行测试,然后我们会开打浏览器打开,了浏览器会显示"正在接受自动换测试软件的控制",测试结束后,浏览器关闭

    PS E:\workpace\units\vuetest\unit-demo> npm run e2e
    
    > unit-demo@1.0.0 e2e E:\workpace\units\vuetest\unit-demo
    > node test/e2e/runner.js
    
    Starting selenium server... started - PID:  19864
    
    [Test] Test Suite
    =====================
    
    Running:  default e2e tests
     √ Element <#app> was visible after 62 milliseconds.
     √ Testing if element <.hello> is present.
     √ Testing if element <h1> contains text: "Welcome to Your Vue.js App".
     √ Testing if element <img> has count: 1
    
    OK. 4 assertions passed. (26.305s)
    
    PS E:\workpace\units\vuetest\unit-demo>
    

    在这里插入图片描述
    开起来有点科幻。

    e2e测试方案目前我接触到的有两种:

    1. Nightwatch+Selenium
    2. PhantomJs + Casperjs

          Nightwatch是一个基于Selenium WebDriver API的e2e自动化测试框架,可以使用js方法与css选择器来编写运行在Selenium服务器上的端到端测试,这种方式会拉起本地的浏览器,就像上面这副图一样,浏览器会显示"正在接受自动换测试软件的控制",当然至于具体打开什么浏览器就要看安装的webdriven是什么了。这种方式另外一个好处就是可以拉起不同浏览器,对不同浏览器进行兼容性测试。

    Nightwatch有两种方式去调起浏览器跑测试

    1. 通过Selenium,调各个浏览器的webDriver,唤起浏览器。这个需要安装java、Selenium、webDriver
    2. 直接通过各个浏览器厂家提供的webDriver驱动浏览器。

          很明显vue-cli初始化工程的时候选择的是第一种方案,具体怎么操作可以去Nightwatch官网看看。

          第二种方案PhantomJs+Casperjs是使用无头浏览器进行测试,PhantomJs我们可以把他看成一个chrome的无头浏览器,Casperjj对phantomjs和webpage模块的封装,让我们可以以链式风格的方式写代码。调用Casperjs相关API来操作应用和浏览器。这种方案特别的地方是可以不用在桌面打开浏览器就可以跑测试案例。具体怎么操作可以去PhantomJs官网看看。
          具体选择哪种方案这个看个人喜好,有的就是要看见实物才放心,那就选择第一种;如果习惯悄悄地干活,那也可以选择第二种,我个人从体验上来说还是更加喜欢第一种,毕竟这种看着别人干活,自己只需要一个命令就搞定的事,还是比较哇咔咔的。

          有了方案后,再来看看这些方案能不能满足e2e的测试需求,大致有那些需求呢?

    1. 和页面的交互,就是模拟各种和页面的交互事件(click,input…),交互完之的页面内容显示是否正确,简单点来说,就是我们平时在哪里手动测试的那些操作,能不能用脚本进行模拟,这也是我们最关心的事。
      答案是完全没问题的,这两种方案都可以模拟各种事件,然后去获取页面内容,然后将内容和自己预想的值进行比较,最后得出测试用例是否通过的结果;
    2. 页面元素是否显示正确,这里需要对元素的属性值和设计稿中的属性值进行对比。
      可以通过获取页面元素以及元素的CSS属性值。去判断这些属性值是否正常,来达到测试目的。

    视觉回归测试

          简单来说就是测试应用整体界面是否达到了UI设计图的要求。
          这个就比较困难了,也是前端自动化测试中最难的部分吧,毕竟这是感官层面上的事。当然也不是没有方案。这里介绍两种方案:
    1. Gemini
    Gemini(https://github.com/gemini-testing/gemini)项目是 Yandex 团队开发的视觉回归测试工具
    2. PhantomCSS
          PhantomCSS(https://github.com/Huddle/PhantomCSS)由 Huddle公司的James Cryer 带领开发团队编写。它依赖于 CasperJS 和 Resemble.JS。
          其实上面两种方式基本原理都是通过截取应用图像然后和基准图像通过像素之间的差异值进行对比,然后得出测试结论,这是在实现的方式和以来的环境他又一定的差异。
          视觉回归测试具体的实现,我们目前还没有针对这两种方案去实践,一方面是最近确实时间上有点紧张,另一方面个人人为,虽然这两种方式能进行自动化测试,但是在实用性上还是存在疑惑的,因为现在的web页面展现的内容相对比较复杂,页面内容也比较丰富。既然有方案就去尝试吧,后面有时间我去实践一下,然后再分享出来和大家共勉。

    性能测试

          前端的性能测试,打开Chrome DevTools就一目了然了:
    在这里插入图片描述
          通过调试窗口,能够清楚的看见页面各种资源的加载情况,看起来比较直观的,但是这还是需要人为的去打开调试窗口,刷新页面,然后通过我们的火眼金睛去发现其中的异常,这和我们所说的自动化测试好像有点不太相符,自动化测试的原则就是,能用代码解决的事,就不要扯其他的,写就完事了。

           先看看前端性能测试技有那些技术指标:

    1. 白屏时间
      用户首次看到网页有内容的时间,即第一次渲染流程完成时间。
    2. 首屏时间:
      用户看到第一屏,即整个网页顶部大小为当前窗口的区域,示完整的时间。
    3. 首资源下载时间
      从开始下载到第一个资源均下载完成的时间,不包括页面绘制时间。
    4. 总资源下载时间
      从开始下载到所有资源均下载完成的时间,不包括页面绘制时间。
    5. 用户可操作时间
      从页面开始加载到用户操作可响应的时间。

          然后根据这些指标来确定测试方案,目前我自己是使用的是 PhantomJs来进行性能测试的
          PhantomJs前端介绍了他是一个Chrome内核的无头浏览器,所谓无头,是指我们在眼再桌面看不见,当然除了获取测试数据外还需要生成一份测试报告,先来看看如何使用phantomjs来获取相关页面加载性能的数据?
    首先安装phantomjs:

    npm install phantomjs
    

    安装好后,我们就可以开始写测试脚本了

    var page = require('webpage').create();
    var loadStartTime =Date.now();
    var pageUrl = 'https://www.taobao.com/';
    var resources = {};
    function analysisLoadTime(page){
        page.evaluate(function() {
            function calculateTime(time){
                return (parseFloat(time)/1000).toFixed(3);
            }
            console.log("页面名称:",document.title);
            console.log("详细耗时:")
            var performance = window.performance.timing;
            var connectEnd = performance.connectEnd
               ,connectStart = performance.connectStart
               ,domComplete=performance.domComplete
               ,domContentLoadedEventEnd=performance.domContentLoadedEventEnd
               ,domContentLoadedEventStart =performance.domContentLoadedEventStart
               ,domInteractive=performance.domInteractive
               ,domLoading = performance.domLoading
               ,domainLookupStart = performance.domainLookupStart
               ,fetchStart = performance.fetchStart
               ,loadEventEnd = performance.loadEventEnd
               ,domainLookupEnd = performance.domainLookupEnd
               ,loadEventStart = performance.loadEventStart
               ,navigationStart = performance.navigationStart
               ,redirectEnd = performance.redirectEnd
               ,redirectStart = performance.redirectStart
               ,requestStart = performance.requestStart
               ,responseEnd = performance.responseEnd
               ,responseStart = performance.responseStart
               ,unloadEventStart = performance.unloadEventStart
               ,unloadEventEnd = performance.unloadEventEnd
               ,secureConnectionStart = performance.secureConnectionStart
            var PromotFotUnloadTime = navigationStart;
            var redirectTime = redirectEnd-redirectStart;
            var AppCacheTime = fetchStart-domInteractive;
            var DNSTime =domainLookupEnd-domainLookupStart
            var TCP_time = connectEnd-connectStart;
            var request_time = responseEnd-requestStart;
            var DomLoad_time =domComplete-domLoading;
            var DomCOntentLoaded_time =domInteractive-domLoading;
            var EventLoad_time = loadEventEnd-loadEventStart
            var operate_time = loadEventEnd-navigationStart;
            var white_time = domLoading-navigationStart
            var first_view_time = domInteractive-navigationStart
            console.log("页面白屏时间:",calculateTime(white_time));
            console.log("首屏时间:",calculateTime(first_view_time));
            console.log("资源请求耗时:",calculateTime(request_time));
            console.log("用户可操作时间:",calculateTime(operate_time));
    
        });    
    }
    page.open(pageUrl, function(status) {
        if(status=="success"){
            var loadFinishTime =(Date.now()-loadStartTime)/1000;
            console.log("页面加载成功,总共耗时["+loadFinishTime+"s]");
            page.evaluate(function() {
                console.log(document.title);
            });
            analysisLoadTime(page)
        }else{
            console.log("页面加载失败")
        }
        phantom.exit();
    });
    page.onConsoleMessage = function(msg){
        console.log(msg);
    }
    page.onResourceRequested = function (req) {
        resources[req.id] = {
            url:req.url,
            startTime:Date.now(),
            endTime:null,
            total:0
        }
    };
    
    page.onResourceReceived = function (res) {
        if(resources[res.id]){
            resources[res.id].endTime = Date.now();
            resources[res.id].total =(resources[res.id].endTime-resources[res.id].startTime)/1000+"s";
            console.log("请求:",res.id,"-->耗时:[",resources[res.id].total,"]s")
        }
       
    };
    
    

          这里面关系相关的API我就不去介绍了,度娘上面很多我就不赘述了。这里主要提一下关键点,就是浏览器自带的window.performance对象,里面包含了当前页面相关性能参数。
          通过phantomjs中加载页面,去获取这个对象,然后对相关数据进行分析就可以得到页面相关的性能参数。当然,每次执行结果会不同,一方面和网络情况有关,另外一方面和缓存有很大关系,这个在写脚本的时候一定要注意到。
          通过上面的的一顿学习,前面我提到的大部分问题都是可以找到比较合适的解决方案,但是还是有一点,就是再浏览器中如果加载有类似OCX,NP插件的话,就测不了。如果谁在这方面有比较好的方案,可以@一下我,相互交流一下,我也会在空余时间继续浏览这方面的测试方案。

    总结

          其实我对前端自动化测试最开始真像我前面提到的那些问题一样,一脸懵逼,但是通过不断的去学习和实践,现在对自动测试已经有了一个比较深刻的认识,面对不同的测试场景,也能想到相关的测试方案。希望这篇文章对前端自动化测试感兴趣而目前又有点懵逼的朋友有一点帮助。关键的一点还是要自己去实践,光看是弄不明白的。请忽略“通假字”。

    展开全文
  • 主要介绍了详解Puppeteer前端自动化测试实践,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
  • 什么是前端自动化测试? 前端编写一段js 去运行生产中的js代码,我们预期会出现的结果与实际出现的结果是否相等,在上线前检测成问题,这样通过代码自动检测,而非人肉点击就是前端自动化测试 前端自动化测试的优势 ...

    下一篇文章
    前端自动化测试(二)TDD与BDD 实todoList
    什么是前端自动化测试?
    前端编写一段js 去运行生产中的js代码,我们预期会出现的结果与实际出现的结果是否相等,在上线前检测成问题,这样通过代码自动检测,而非人肉点击就是前端自动化测试

    前端自动化测试的优势

    ​ 1.更好的代码组织,项目的可维护性增强。
    ​ 2.更小的Bug出现概率,尤其是回归测试中的Bug。
    ​ 3.修改工程质量差的项目,更加安全。
    4.项目具备潜在的文档特性。
    ​ 5.扩展前端知识面。

    学习自动化测试更有利于读懂各种源码,测试用例其实就是源码的文档,他详细告诉了实现的功能

    tips:下列指定版本只是为了,以后学习不冲突

    一、安装

      npm i jest@24.8.0 -D
    

    二、配置jest

    // 初始化配置
    npx jest --init
    

    package.json

    // 配置启动,加上--watchAll会自动监听所有test文件改动并运行 默认是a模式
    // --watch 自动是o模式
    // 配置任意一个,就会默认为该模式,同时在watch Usage中消失
      "scripts": {
        "test": "jest --watchAll" 
      },
    

    jest 命令行使用

    // 按 f 以仅运行失败的测试。
    › Press f to run only failed tests.
    // 按 o 仅运行与已更改文件相关的测试。
     › Press o to only run tests related to changed files.
    //  按 p 以按文件名正则表达式模式进行筛选
    //只想执行那些测试文件就可以用p模式 
     › Press p to filter by a filename regex pattern.
    //  按 t 以按测试名称 regex 模式进行筛选
    // 只想执行那些测试用例就可以用t模式 也叫feilter模式
     › Press t to filter by a test name regex pattern.
    //  按q退出监视
     › Press q to quit watch mode.
    //  按u确定更新快照(只在快照时显示)
     › Press u to update failing snapshots.
    //  按i以交互方式更新失败的快照。
      Press i to update failing snapshots interactively.
    // 按 s 跳过当前用例
       Press s to skip the current test.
    //  按 Enter 可触发测试运行。
     › Press Enter to trigger a test run.
    //  按w展示所有使用方式
     Watch Usage: Press w to show more.
    

    当没有改动或者错误的文件时,再次按对应的就能退出该模式
    按 o的时候报错
    –watch is not supported without git/hg, please use --watchAll
    需要在git中使用,通过git来记录修改的文件所以要安装git并初始化git文件同时需要提交到本地

    1. 查看测试覆盖率
      npx jest --coverage

    在jest.config.js中 添加

     coverageDirectory: "coverage",//生成代码覆盖率报告会放在coverage目录下
    

    会在根目录生成coverage文件夹,点击打开lcov-report 下的 index.html 就能看见图形化界面

    1. jest 默认是commoJs的规范是在node环境下运行,如果想用es6的语法,需要安装babel转换
      在你运行jest时,jest内部封装了,会运行 jest(babel-jest)检测你是否安装了babel-core,就会去取.babelrc的配置,
      运行测试前结合babel先把代码进行一次转化
    cnpm install @babel/core@7.4.5 @babel/preset-env@7.4.5 -D
    

    配置.babelrc

    在根目录新建一个文件.babelrc

    {
      "presets": [
        [
          "@babel/preset-env",{
          "targets":{
            "node":"current"
          }
        }
      ]
      ]
    }
    

    三、初识jest

    我们简单写点测试用例来认识一下jest

    新建一个math.js 用来创建 加减方法

    export function add(x,y){
      return x+y
    }
    
    export function minus(x,y){
      return x-y
    }
    export function multi(x,y){
      return x*y
    }
    

    下面我们就来测试一下这个math.js

    //引入需要测试的方法
    import {add,minus,multi} from './math'
      
    test('测试加法 3+3',()=>{
        //我期望3+3得到6
      expect(add(3,3)).toBe(6)
    })
      
    test('测试减法 3-3',()=>{
      expect(minus(3,3)).toBe(0)
    })
      
    test('测试乘法 3*3',()=>{
      expect(multi(3,3)).toBe(9)
    })
    

    上面的测试是否很简单,没错前端测试就是这样简单明了,

    通过代码去执行方法,或模拟用户行为

    test原理 简单用原生js来实现一下上述代码

    import {add,minus,multi} from './math'
    
    var result = add(3,7);
    var expected = 10;
    if(result!== expected){
      throw Error(`3+7应该等于${expected},但是结果却是${result}`)
    }
    
    var result = minus(3,3);
    var expected = 0;
    if(result!== expected){
      throw Error(`3-3应该等于${expected},但是结果却是${result}`)
    }
    
    
    
    // 实现一个 
    //  expect(add(3,3)).toBe(6)
    //  expect(minus(6,3)).toBe(3)
    function expect (result){
      return {
        toBe: function(actual){
            if(result!==actual){
              throw new Error(`预期值和实际值不相等 预期${actual} 结果却是${result}`)
            }
        }
      }
    }
    //  expect(add(3,3)).toBe(6)
    
    // 进一步完善
    function test (desc,fn){
      try{
        fn()
        console.log(`${desc}通过测试`);
      } catch(e){
        console.log(`${desc}没有通过测试 ${e}`);
      }
    }
    
    test('测试加法 3+3',()=>{
      expect(add(3,3)).toBe(6)
    })
    

    接下来让我们简单的认识一部分jest的匹配器,

    jest匹配器

    
      
    test('测试10与10匹配',()=>{
      //toBe 匹配器
      //类似 object.is  ===
      // 只能匹配值,不能匹配对象等引用
      expect(10).toBe(10)
    })
    
    
    test ('测试对象内容相等',()=>{
      //toEqual 匹配器
      // 能匹配值,对象等引用
      const a = {one:1};
      expect(a).toEqual({one:1})
    })
    
    test ('测试内容为null',()=>{
      //toBeNull 匹配器
      const a = null;
      expect(a).toBeNull();
    })
    
    test ('测试内容为undefined',()=>{
      //toBeUndefined 匹配器
      const a = undefined;
      expect(a).toBeUndefined();
    })
    
    test ('测试内容为defined',()=>{
      //toBeDefined 匹配器
      const a = null;
      expect(a).toBeDefined();
    })
    
    test("测试内容为真",()=>{
      // toBeTruthy 匹配器
      const a = 1;
      expect(a).toBeTruthy()
    })
    test("测试内容为假",()=>{
      // toBeFalsy 匹配器
      const a = 0;
      expect(a).toBeFalsy()
    })
    
    test("不为真",()=>{
      // not 匹配器取反操作
      const a = 1
      expect(a).not.toBeFalsy()
    })
    // 数字相关匹配器
    
    test('count大于9',()=>{
      const count = 10
      expect(count).toBeGreaterThan(9);
    })
    
    test('count小于9',()=>{
      // toBeLessThanOrEqual
      const count = 8
      expect(count).toBeLessThan(9);
    })
    test('count大于等于9',()=>{
      const count = 9
      expect(count).toBeGreaterThanOrEqual(9);
    })
    
    
    // js运算错误示例
    test("0.1+0.2",()=>{
      const a = 0.1
      const b = 0.2
      // expect(a+b).toEqual(0.3)
      /* 
       Expected: 0.3
        Received: 0.30000000000000004
    */
    // 对于浮点型计算匹配需要使用
    // toBeCloseTo
    expect(a+b).toBeCloseTo(0.3)
    })
    
    // String 相关匹配器
    test("str中包含字符",()=>{
        //toMatch 可以是正则表达式
        const str = "www.baidu.com"
        // expect(str).toMatch('baidu')
         expect(str).toMatch(/baid/)
    })
    
    // 数组相关匹配器
    test("数组中包含某一项",()=>{
      const arr = ['a','b','c']
      // 可以set后在匹配
      expect(arr).toContain('a')
    })
    
    // 异常
    const throwNewErrorFunc = ()=>{
      throw new Error('this is a new error')
    }
    test('toThorow',()=>{
      expect(throwNewErrorFunc).toThrow()
      // 如果要填写内容意思就是匹配异常内容相当,也可以是正则表达式
    })
    

    jest中的钩子函数

    大致分为 :

    beforeAll 所有测试用例之前

    beforeEach 每个测试用例执行前都调用

    afterEach 每个测试用例执行之后

    afterAll 所有测试用执行之后

    下面来测试一下执行顺序

    ​ 对于归类分组,你可以手动自己分文件,来归类,也可以用describe来分类,

    每一个describe都是一个单独的作用域,可以作用于,下面的所有的describe,
    同级的互不影响,每个describe都可以拥有独自的钩子函数,执行顺序,先执行外部,再执行内部

    新建一个文件counter.js

    //模拟用于测试的方法
    export default class Counter{
      constructor(){
        this.number = 0
      }
      addOne(){
        this.number+=1
      }
      addTwo(){
        this.number+=2
      }
      minusOne(){
        this.number-=1
      }
      minusTwo(){
        this.number-=2
      }
    }
    

    测试该文件 新建 counter.test.js

    import Counter from "./counter"
    
    // 相同的归类分组
      // 2种方式,一种分文件,一种是用describe分组
    describe('测试counter的相关代码',()=>{ 
      console.log('测试counter的相关代码');
    
      let counter  = null
    beforeAll(()=>{
      // 所有测试用例之前
      console.log('beforeAll');
       
    })
    beforeEach(()=>{
      // 每个测试用例执行前都调用
      console.log('beforeEach');
      counter = new Counter()
    })
    afterEach(()=>{
      // 每个测试用例执行之后
      console.log('afterEach');
    })
    afterAll(()=>{
        // 所有测试用例之后
      console.log('AfterAll');
    })
    
    
        describe('测试增加的代码',()=>{
          beforeEach(()=>{
            console.log('beforeEach to add');
          })
          afterEach(()=>{
            console.log('afterEach to add');
          })
          afterAll(()=>{
            console.log('afterAll to add');
          })
          beforeAll(()=>{
            console.log('beforeAll to add');
          })
          console.log('测试增加的代码');
          test('测试Counter中的addOne方法',()=>{
            console.log('测试Counter中的addOne方法');
            counter.addOne();
            expect(counter.number).toBe(1)
          })
          test('测试Counter中的addTwo方法',()=>{
            console.log('测试Counter中的addTwo方法');
            counter.addTwo();
            expect(counter.number).toBe(2)
          })
        })
    
        describe('测试减少的代码',()=>{
          console.log('测试减少的代码');
          test('测试Counter中的minusOne方法',()=>{
            console.log('测试Counter中的minusOne方法');
            counter.minusOne();
            expect(counter.number).toBe(-1)
          })
          
            test('测试Counter中的minusTwo方法',()=>{
              console.log('测试Counter中的minusTwo方法');
              counter.minusTwo();
              expect(counter.number).toBe(-2)
            })
        })
      })
      // 如果只想执行某一个用例,可以用test.only来修饰 only可以同时存在多个
    /* 
    // 每一个describe都是一个单独的作用域,可以作用于,下面的所有的describe,
      同级的互不影响,每个describe都可以拥有独自的钩子函数,执行顺序,先执行外部,再执行内部
    
    
    */
    /* 
    执行顺序如下:
      console.log counter.test.js:7
        测试counter的相关代码
    
      console.log counter.test.js:44
        测试增加的代码
    
      console.log counter.test.js:58
        测试减少的代码
    
      console.log counter.test.js:12
        beforeAll
    
      console.log counter.test.js:42
        beforeAll to add
    
      console.log counter.test.js:18
        beforeEach
    
      console.log counter.test.js:33
        beforeEach to add
    
      console.log counter.test.js:46
        测试Counter中的addOne方法
    
      console.log counter.test.js:36
        afterEach to add
    
      console.log counter.test.js:23
        afterEach
    
      console.log counter.test.js:39
        afterAll to add
    
      console.log counter.test.js:18
        beforeEach
    
      console.log counter.test.js:60
        测试Counter中的minusOne方法
    
      console.log counter.test.js:23
        afterEach
    
      console.log counter.test.js:27
        AfterAll
    */
    

    jest测试异步代码

    安装 axios

    npm i axios -D
    

    新建文件 fetchData用于模拟异步代码

        import axios from 'axios'
    //该接口返回值
    //{
     // "success": true
    //}
    //回调类型的异步函数
    export const fetchDataCbk = function(fn){
      
      axios.get('http://www.dell-lee.com/react/api/demo.json')
    .then(function(response) {
      fn(response.data)
    })
    }
    //无回调类型的异步函数
    export const fetchData = function(){
      return axios.get('http://www.dell-lee.com/react/api/demo.json')
    
    }
    

    测试文件fetchData.test.js

    import {fetchData,fetchDataCbk} from "./fetchData"
    // 回调类型的异步函数测试
    // 只有执行到done执行才结束
    test('用done来测试返回结果为{success:true}',(done)=>{
      fetchDataCbk((data)=>{
        expect(data).toEqual({
          success: true
        })
        done()
      })
      
    })
    
    // 无回调类型的异步函数测试
    //多种实现方法
    test('测试返回结果为{success:true}',()=>{
      return fetchData().then((res)=>{
        expect(res.data).toEqual({
          success:true
        })
      })
      
    })
    
    test('测试返回结果为{success:true}',async()=>{
     const res = await fetchData()
        expect(res.data).toEqual({
          success:true
        })
      
    })
    test('测试返回结果为{success:true}',()=>{
      return expect(fetchData()).resolves.toMatchObject({
      data:{
        success: true
      }
      })
      
    })
    test('测试返回结果为{success:true}',async()=>{
      await expect(fetchData()).resolves.toMatchObject({
      data:{
        success: true
      }
      })
      
    })
     //测试返回404
    test('测试返回结果为 404',()=>{
      expect.assertions(1);//测试用例必须执行一次
      return fetchData().catch((e)=>{
        expect(e.toString().indexOf('404')!==-1).toBe(true)
      })
      
    })
    test('测试返回结果为 404',()=>{
      return  expect(fetchData()).rejects.toThrow()
      
    })
    

    jest 中的mock

    当我们测试请求时,我们并不需要测试接口返回的数据,接口测试是属于后端的测试了,我们只关心,代码是否正常执行

    而且如果都去请求,那么测试效率会很慢,这个时候我们就需要用mock来模拟ajax请求,不去请求真实的ajax

    新建文件 demo.js

    import Axios from "axios"
    
    export const runCallback  = function (callBack){
      callBack()
    }
    export const createObject  = function (callBack){
      new callBack()
    }
    
    export const getData = function(){
      return Axios.get('/api')
    }
    

    测试 demo.test.js

    import {runCallback,createObject,getData} from './demo'
    import Axios from "axios"
    
      jest.mock('axios') //模拟axios
    //回调的异步函数
    test('测试runCallback', ()=>{
     
      // 可以自由定义返回值
      const func = jest.fn(()=>{
        return '456'
      })
        // 上面等同提出来下面
      // func.mockImplementation(()=>{
      //   return '456'
      // })
    
      // 如果只想返回this
      // func.mockReturnThis()
    
      // func.mockReturnValueOnce('一次返回')
      // func.mockReturnValue('456') 定义返回值
      	runCallback(func)
     // 通过jest.fn 创建的mock函数,可以用toBeCallEd捕获这个函数是否被调用了
      expect(func).toBeCalled()
      // expect(func).toBeCalledWith('value') 每一次调用传入的都是value
      console.log(func.mock);
      /*打印的mock:
       {
          calls: [ [] ], //包含调用时,传递的参数,可以通过判断calls的length来判断调用了几次
          instances: [ undefined ],指func 运行时,this的指向
          invocationCallOrder: [ 1 ],调用的顺序
          results: [ { type: 'return', value: '456' } ] 执行调用的返回值
        }
      */
    })
    test('测试createObject',()=>{
      const fn = jest.fn()
      createObject(fn)
      console.log(fn.mock);
    })
    
    test('测试getData',async()=>{
      // 模拟返回,不会去请求真实的数据
      // mockResolvedValueOnce
      Axios.get.mockResolvedValue({data:'hello'})
      await getData().then((data)=>{
        expect(data).toEqual({data:'hello'})
      })
    })
    

    我们除开上面的模拟axios的方式,我们还可以通过模拟异步函数,通过使用模拟的异步函数来达到不请求axios的效果

    被测试文件 demo.js

    import Axios from "axios"
    
    export const fetchData = function(){
      return Axios.get('/api')
    }
    
    export const getNumber = function(){
      return 123
    }
    

    在同级创建一个_mocks_文件夹

    同样创建一个demo.js来模拟原demo.js 的异步请求

    export const fetchData = ()=>{
      return new Promise((resolved,reject)=>{
        resolved("(function(){return '123'})()")
      })
    }
    

    测试文件 demo.test.js

    jest.mock('./demo.js') //模拟后会去查找__mocks__下的demo.js,而非真实的的demo.js
    // 或者直接将config.js中的automock 改成true 自动开启模拟
    // unmok  不模拟
    import {fetchData} from './demo'
    // 当我们开启模拟时,如果想让模拟文件中异步需要模拟,而同步不需要模拟就需要下面这样引入同步方法
    const {getNumber} = jest.requireActual('./demo.js') //引入真正的demo
    
    
    
    test('测试异步fetchData',async()=>{
      return fetchData().then(data=>{
        console.log(data);
        expect(eval(data)).toEqual('123')
      })
    })
    test('测试同步getNumber',async()=>{
      expect(getNumber()).toBe(123)
    })
    

    快照 snapshot

    故名思意,就是类似拍照一样,给你的代码生成一个副本,当代码有所变动,就去与副本中的代码对比,判断是否需要本次的修改,什么时候使用快照呢,当你的代码基本完善,无需修改时,就可以生成一个快照,以后出现代码修改,就可以通过快照的检测,知道那个文件发生了改动

    测试文件 demo.js

    export const generateConfig= ()=>{
      return {
        sever:"localhost",
        port:8080,
        proxy:8081
      }
    }
    export const generateAnotherConfig= ()=>{
      return {
        sever:"localhost",
        port:8080,
        proxy:8082
      }
    }
    
    export const generateTimeConfig= ()=>{
      return {
        sever:"localhost",
        port:8080,
        proxy:8084,
        time:new Date()
      }
    }
    

    测试文件 demo.test.js

    import {
      generateConfig,
      generateAnotherConfig,
      generateTimeConfig
    } from "./demo";
    
    test("测试generateConfig 函数", () => {
      expect(generateConfig()).toMatchSnapshot();
    });
    //假设,测试一个配置文件,如果你修改了配置文件,如果使用的是toEqual(),
    /*那么每次修改配置,都需要同步修改test,这样很麻烦,使用toMatchSnapshot()
    (快照), 会在根目录生成一个snapshots文件保存运行时,的测试的配置项代码,就
    好像,拍了一个照片,之后就会和对比新快照,和旧快照是否一致,判断测试用例是否
    通过, 
    假设这时修改了配置
     1 snapshot failed from 1 test suite. Inspect your code changes or press `u` to update them.
     你只需打开w操作指令,按u表示,更新快照,就可以再次通过,
     类似于提示你,是否要确实这次修改
     
     当出现多个快照时,如果不想所有快照都更新,想一个一个确认更新,这个时候
     w 中会多出一个 i 模式,让你进入到一个一个确认下,这时再按u就表示确认更新快照
     如果感觉是错的,或者不确定,可以按s跳过该快照用例
     */
    test("测试generateAnotherConfig 函数", () => {
      expect(generateAnotherConfig()).toMatchSnapshot();
    });
    
    // 当配置项中存在new Date() 这种动态变化的参数,就需要配置去忽略它,不然无法通过
    test("测试generateTimeConfig 函数", () => {
      expect(generateTimeConfig()).toMatchSnapshot({
        time: expect.any(Date) //任意date类型都行
      });
    });
    
    //生成行内快照,下面的object就是运行后生成的快照
    // 前置条件需要安装包cnpm i prettier@1.18.2 -D
    // 行内快照 用toMatchInlineSnapshot不会单独生成一个文件,而是把快照直接
    // 生成到函数内,
    test("测试generateAnotherConfig 函数", () => {
      expect(generateAnotherConfig()).toMatchInlineSnapshot(`
        Object {
          "port": 8080,
          "proxy": 8082,
          "sever": "localhost",
        }
      `);
    });
    
    

    jest中的timer

    当我们测试延时器等等时,不可能去等待时间再执行,这样测试效率会极低,所以jest提供了如下方式来快捷的测试timer

    被测试文件 timer.js

    export const timer = (callback)=>{
      
     setTimeout(()=>{
        callback()
        setTimeout(()=>{
          callback()
        },3000)
      },3000);
    }
    

    测试文件 timer.test.js

    import {timer} from './timer'
    
    beforeEach(()=>{
      
        jest.useFakeTimers(); //每个测试用例执行前,初始一下防止影响
      
    })
    test('测试定时器',()=>{
      const  fn = jest.fn()
      timer(fn);
      // jest.runAllTimers(); //让定时器立即执行,与上面的use配对使用
      // jest.runOnlyPendingTimers(); //只执行队列中存在的timer
      jest.advanceTimersByTime(3000)//快进定时器
      expect(fn).toHaveBeenCalledTimes(1) //fn只调用一次
      jest.advanceTimersByTime(3000) //快进是在上一个的基础上,存在多个测试用例时,可能会印象下面的,所以我们需要在运行之前重置一下
      expect(fn).toHaveBeenCalledTimes(2) 
    })
    

    jest中类的mock

    新建模拟类的测试文件

    util.js

     export default class Util{
     	 init(){
    
     	 	}
      	a(){
    
     	 }
      	b(){
    
      	}
    }
    

    新建文件 demo.js

    import Util from './util'
    const demoFunction = (a,b)=>{
      const util = new Util();
      util.a(a)
      util.b(b)
    }
    export default demoFunction
    

    测试文件 demo.test.js

    import demoFunction from './demo'
    import Util from './util'
    
    jest.mock('./util')
    //jest.mock 发现uitl是一个类,会自动把类的构造函数和方法变成jest.fn()
    /* 
      const Util= jest.fn()
      until.prototype.a = jest.fn()
      until.prototype.b = jest.fn()
      如果不满意默认处理,可以自定义在文件__mock__下util.js自行模拟
      如果不是很复杂可以直接传递第二个参数,就会执行第二个参数的代码
      jest.mock('./util',()=>{
         const Util= jest.fn()
      Util.prototype..a = jest.fn()
      Util.prototype..b = jest.fn() 
      return Util
      })
    */
    
    // 这里测试的关注点是是否有执行,如果类里a b 方法很复杂就会很耗性能,而他们执行的结果并非所关心的,所以用mock模拟
    test('测试 demoFunction',()=>{
      demoFunction()
      expect(Util).toHaveBeenCalled() //是否执行过
      expect(Util.mock.instances[0].a).toHaveBeenCalled()
      expect(Util.mock.instances[0].b).toHaveBeenCalled()
    })
    
    
    

    这里引入2种概念 单元测试,和集成测试

    单元测试
    只关注,该单元的代码,对于外部的引入不关心,如果对性能有影响就会用mock
    就想上面的测试demoFunction,我只关心有没有执行过a,b方法并不关心执行的结果

    简单说就是对单一功能的测试

    集成测试
    对单元中所有都测试
    我不仅要执行,同时也关心执行后对该单元的影响

    对多种功能的集合测试

    测试驱动开发

    Test Driven Development (TDD) 测试驱动开发
    开发流程

    1. 编写测试用例
    2. 运行测试,测试用例无法通过测试
    3. 编写代码,使测试用例通过测试
    4. 优化代码,完成开发
    5. 新添加,继续重复上述步骤

    TDD的优势

    1. 长期减少回归bug
    2. 代码质量更好
    3. 测试覆盖率高

    行为驱动开发

    BDD(Behavior Driven Developmen)

    先编写代码,基于用户的行为去编写测试代码

    TDD 与BDD 区别

    TDD:

    1. 先写测试再写代码
    2. 一般结合单元测试使用,是白盒测试
    3. 测试重点在代码
    4. 安全低(重点在代码对与用户交互给人的安全低)
    5. 测试速度快

    BDD:

    1. 先写代码再写测试
    2. 一般结合集成测试使用,是黑盒测试
    3. 测试重点在UI(DOM)
    4. 安全感高(基于用户使用的测试,给人的安全感高)
    5. 测试速度慢

    完成上述基础的学习,那么看懂下面的代码就是轻而易举了

    接下来我们将在vue中使用jest

    vue自动化测试

    创建一个vue项目,勾选上jest,生成一个包含jest的vue文件

    更多请查看 Vue Test Utils 文档

    常用api

    mount
    创建一个包含被挂载和渲染的 Vue 组件的 Wrapper。
    shallowMount  
    和 mount 一样,创建一个包含被挂载和渲染的 Vue 组件的 Wrapper,不同的是被存根的子组件
    wrapper options
    	wrapper.vm
                    这是该 Vue 实例。你可以通过 wrapper.vm 访问一个实例所有的方法和属性。这只存在于 Vue 组件包裹器                  或绑定了 Vue 组件包裹器的 HTMLElement 中
           .contains
    				判断 Wrapper 是否包含了一个匹配选择器的元素或组件。
    		.emitted
    				执行一个自定义事件。返回一个包含由 Wrapper vm 触发的自定义事件的对象。
             .trigger
    				为 WrapperArray 的每个 Wrapper DOM 节点都触发一个事件。
    		.find
    				返回匹配选择器的第一个 DOM 节点或 Vue 组件的 Wrapper。
             .findAll
    				返回所有,类似jquery
             .props   
    				返回 Wrapper vm 的 props 对象。如果提供了 key,则返回这个 key 对应的值
    

    配置 jest.config.js

    module.exports = {
      preset: '@vue/cli-plugin-unit-jest',
        moduleFileExtensions: [ 'js', 'jsx', 'json', 'vue' ], //查找文件的后缀
        transform: { //匹配到对应的后缀文件使用对应的转化
          '^.+\\.vue$': 'vue-jest', //解析vue语法,
          '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',//将静态资源转换为字符
          '^.+\\.jsx?$': 'babel-jest' //es6语法转换es5
        },
        transformIgnorePatterns: [ //不需要转换的文件
          '/node_modules/'
        ],
        moduleNameMapper: { //路径映射,
          '^@/(.*)$': '<rootDir>/src/$1'
        },
        snapshotSerializers: [ //对快照vue语法编译
          'jest-serializer-vue'
        ],
        testMatch: [ //测试文件位置,满足下列规则就当成测试文件
          '**/tests/unit/**/*.(spec|test).(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
        ],
        testURL: 'http://localhost/', //测试下的浏览器地址
        watchPlugins: [ //添加交互选择
          'jest-watch-typeahead/filename',
          'jest-watch-typeahead/testname'
        ]
      }
      
    
    

    如果你是手动按照最开始那种安装jest, 那么 jest-watch-typeahead/filename、 jest-watch-typeahead/testname、jest-serializer-vue、babel-jest、jest-transform-stub、@vue/cli-plugin-unit-jest、vue-jest 这些包就需要你手动安装一下,

    packge.json

    如果你的项目没有git初始化,请使用–watchAll

    接下来我们找到vue的tests 示例 发现和jest不一样,不慌,我们先用另一个写法实现一下,vue的test-utils, 为我们提供了一些用于测试的api可以查看官网,

    import { shallowMount } from '@vue/test-utils' 
    // shallowMount浅渲染,只渲染当前组件,不渲染包含的子组件,适合单元测试,
    // mount 全渲染,适合集成测试
    import HelloWorld from '@/components/HelloWorld.vue'
    
    describe('HelloWorld.vue', () => {
      it('renders props.msg when passed', () => {
        const msg = 'new message'
        //一个 Wrapper 是一个包括了一个挂载组件或 vnode,以及测试该组件或 vnode 的方法。
        //wrapper.vm,可以访问到vue的实例
        const wrapper = shallowMount(HelloWorld, {
          propsData: { msg }
        })
        //返回 Wrapper 的文本内容。渲染的文本中包含msg
        expect(wrapper.text()).toMatch(msg)
      })
    })
    
    

    //模拟一个上述实现

    import Vue from 'vue'
    import HelloWorld from ' @/components/HelloWorld '
    describe( 'HelloWorld.vue', () => {
    	it(' renders props.msg when passed',() =>{
            const root = document. createElement( 'div');
            root. className ='root' ;
            document.body.appendChild( root) ;
            new Vue({
             render: h => h(HelloWorld,{
                 props:{
                     msg: 'dell lee'
                 }
             })
                }).$mount( '.root' )
            consloe.log(document.body.innerHTML);
            expect ( document.getElementsByClassName( 'hello' ). length). toBe(1);
          })
    })
    
    
    

    测试 vuex

    对store进行的单元测试

    store.js

    import Vue from 'vue'
    import Vuex from 'vuex'
    
    Vue.use(Vuex)
    
    const store = new Vuex.Store({
      state:{
        value:1
      },
      mutations:{
        ADD:(state,data)=>{
          state.value = data
        }
      },
      actions:{
        commitAdd:({commit},data)=>{
          commit("ADD")
        }
      }
    })
    
    export default store
    
    import store from '@/store'
    it(`当 store 执行add发生变化`, ()=>{
    const value = 123 ;
    store.commit( ' ADD', value);
    expect( store.state.value).toBe( value);
    })
    
    

    当你的测试文件使用了store,需要在挂载的时候,将store传入,否则store会找不到

    import store from '../../store'
    it(`
    	使用store
    	`,()=>{
        const wrapper = mount(todoList,{store})
    	........
    })
    

    vue中的异步测试

    我们不需要请求真实的的地址,我们只需要在tests文件同级建立一个新文件夹_mocks_下面新建一个axios.js

    axios.js 示例

    export defult get(url){ //模拟get方法
        if (url === '/axios.json'){
            return new promise((resolve)=>{
                const data = [{value:1},{value:2}]
                resolve(data)
            })
        }
    }
    

    在test文件调用mount时会执行,mounted,在执行请求获取数据时,test会优先查看_mocks_ 下面的axios.js 去替换真实的请求

    import store from '../../store'
    it(`
    	1. 用户进入页面时,请求远程数据
    	2. 列表应该展示远程返回的数据
    	`,(done)=>{
        const wrapper = mount(todoList,{store})
        //异步测试,需要使用nextTick和done来等待挂载成功后再执行,jest不会自己等待异步
        wrapper.vm.$nextTick(()=>{
        const listItems = wrapper.findAll('[data-test=item]')
        expect(listItems.length).toBe(2)
             done()
        })
    })
    

    如果存在定时器

    模拟的测试组件

    <template>
    		<div>
                <ul>
                    <li v-for="item in data" data-test="item">{{item}}</li>
       		 </ul>
        </div>
    </template>
    <script>
    export defult {
     data(){
    	return {
    	data:[]
    	}
    }
    method:{
    	getList(){
    	setTimeout(()=>{
    		axios.get('/axios.json').then((res)=>{
    			this.data = res.data
    		})
    	},4000)
    	}
    }
    }
    </script>
        
    

    错误示例

    在测试代码存在异步的代码,jest并不会去等待定时器执行完,会直接忽略,如果需要,需要使用done()

    这样确实可以,但是会等待4秒并不是我们想要的结果

    it(`
    	1. 用户进入页面时,等待4秒
    	2. 列表应该展示远程返回的数据
    	`,(done)=>{
        const wrapper = mount(todoList)
        	setTimeout(()=>{
    		  const listItems = wrapper.findAll('[data-test=item]')
        		expect(listItems.length).toBe(2)
                done()
    	},4000)
       
    })
    

    正确示例

    beforEach(()=>{
    	jest.useFakeTimets() //用于模拟定时器
    })
    it(`
    	1. 用户进入页面时,等待4秒
    	2. 列表应该展示远程返回的数据
    	`,(done)=>{
        const wrapper = mount(todoList)
     	jest.tunAllTimers() //如果遇到timers让他立即执行
         wrapper.vm.$nextTick(()=>{
        const listItems = wrapper.findAll('[data-test=item]')
        expect(listItems.length).toBe(2)
             done()
        })
    })
    

    下一篇文章
    前端自动化测试(二)TDD与BDD 实战todoList

    展开全文
  • 前端自动化测试实践01—持续集成之jest自动化测试环境搭建 文章目录前端自动化测试实践01—持续集成之jest自动化测试环境搭建1. 概念1.1 持续集成&持续交付&持续部署1.2 持续集成 & 前端自动化测试1.2 ...

    前端自动化测试实践01—持续集成之jest自动化测试环境搭建

    Write By CS逍遥剑仙
    我的主页: www.csxiaoyao.com
    GitHub: github.com/csxiaoyaojianxian
    Email: sunjianfeng@csxiaoyao.com

    本节代码地址 https://github.com/csxiaoyaojianxian/JavaScriptStudy 下的自动化测试目录

    1. 概念

    1.1 持续集成&持续交付&持续部署

    在互联网时代软件从开发到上线,后续迭代更新,已经形成了一套近乎标准的流程,其中最重要的流程就是持续集成(Continuous integration,简称CI)。"持续"的核心思想在于:在事先难以完全了解完整正确的需求时,干脆把大项目分割成小块完成,并加快交付的速度和频率,使其尽早在下个环节得到验证,若发现问题能够尽早返工。

    对于持续集成、持续交付和持续部署三个从敏捷思想中提出的概念,此处举个在知乎上看到的很形象例子:装修厨房,铺地砖时边角地砖要切割大小,如果一次全切割完再铺上去,发现尺寸有误时的浪费和返工成本就大了,不如切一块铺一块,这就是持续集成;装修厨房有很多部分,每个部分都有检测手段,如地砖铺完了要测试漏水与否,线路铺完了要通电测试电路通顺,水管装好了也要测试冷水热水,如果等全部装完了再测,出现问题可能会互相影响,比如电路不行可能要把地砖给挖开,如果每完成一部分就测试,这是持续部署;全部装修完了等待验收,客户发现地砖颜色不合意,水池太小,灶台位置不对,返工吗?所以不如每完成一部分就试用验收,这就是持续交付。这三个概念强调在软件开发过程中,通过技术手段自动化这三个工作,加快交付速度。

    具体来说,持续集成是一种软件开发实践,强调开发人员在提交新代码后立刻进行构建、自动化测试等,频繁自动将代码集成到主干和生产环境,降低一次性集成过多代码带来的风险。

    持续交付在持续集成的基础上,将集成后的代码部署到更贴近真实运行环境的"类生产环境"中。比如把完成单元测试的代码部署包含完整功能的预发布环境中运行更多更完整的集成测试,甚至是人工测试,通过后继续手动部署到生产环境中。

    持续部署则是在持续交付的基础上,把部署到生产环境的过程自动化。

    1.2 持续集成 & 前端自动化测试

    持续集成是互联网软件开发上线流程中的核心一环,在1.1节介绍中也不难看出,自动化测试是持续集成得以实现的核心步骤,缺乏了自动化测试,持续集成自然无从谈起。

    在日常的开发中,前端错综复杂的变化引发的bug往往令开发者头疼,或多或少经历过 修完东墙西墙倒 的经历,此时前端自动化测试就显得非常重要。前端的自动化测试无非也是编写测试用例,在持续集成时执行跑通全部测试用例。如果是一个短平快的小项目,引入前端自动化测试,编写测试用例,无疑只会增加开发成本,然而当项目扩大、迭代频繁、逻辑复杂、需求反复变更的情况下,回归测试的成本是巨额的,自动化测试的优势就能体现出来。

    自动化测试的收益 = 迭代次数 * 全手动执行成本 - 首次自动化成本 - 维护次数 * 维护成本
    

    尽早引入前端自动化测试不仅能够减少项目 bug 出现概率(尤其是回归测试中的 bug),还能更好地进行代码组织,增强项目的可维护性,尤其对于工程质量较差的项目,收益是巨大的;如果将其应用于持续集成中,commit 触发自动执行测试脚本,还能大幅提升团队的开发效率。

    1.2 分类

    前端自动化测试可以按照开发模式分两类:TDD (Test-Driven Development) 测试驱动开发BDD (Behavior Driven Development) 行为驱动开发。TDD 顾名思义,开发者根据需求先编写测试用例,再逐步开发,最终满足全部测试用例的需求。刚开始的时候,只有测试用例,未进行功能开发,执行测试用例,满屏是红色的测试用例不通过提示,随着测试用例被满足变绿,最终全部变绿,功能开发完成,因此前端自动化测试也被叫做 Red-Green Development。测试还可以按照用例粒度分为 单元测试 (Unit Test)集成测试 (Integration Test)端到端测试 (End to End Test)

    1.2.1 TDD (Test-Driven Development) 测试驱动开发

    1. TDD 先写测试再写代码,单位是模块,多用于 单元测试
    2. 重点在测试代码,属于 白盒测试
    3. 测试内容是模块,速度快,但是忽略模块间依赖,安全感低

    TDD 有以下优势:

    1. 长期减少回归BUG
    2. 代码质量会更好(组织、可维护性)
    3. 测试覆盖率高

    1.2.2 BDD (Behavior Driven Development) 行为驱动开发

    1. BDD 先写代码再写测试,测试单位是功能,多用于 集成测试
    2. 重点在测试 UI(DOM)功能,属于 黑盒测试
    3. 测试内容是整套操作流程,速度慢,往往需要多个模块配合,安全感高

    1.3 工具选择

    前端近几年涌现出很多优秀的测试工具:

    • karma – Google Angular团队开发的测试运行平台,配置简单灵活,能够很方便在多个真实浏览器中运行测试

    • mocha – 很优秀的测试框架,有完善的生态系统,简单的测试组织方式,不对断言库和工具做任何限制,非常灵活

    • jest – facebook出品的大而全的测试框架,React官方推荐的单元测试框架,配置简单运行速度快

    还有很多其他的前端测试框架,但大同小异,无非是对断言和测试桩等工具的集成度不同,论成熟度首推 mocha,论效率首推 jest。

    jest是 Facebook 开源的 JavaScript 测试框架,它自动集成了断言、JsDom、覆盖率报告等开发者所需要的所有测试工具,是一款几乎零配置的测试框架,而且速度很快,此处选择 jest 作为测试工具。

    2. jest 环境搭建

    2.1 jest 安装

    jest 需要自动运行测试脚本,node 环境是必不可少的,如果从头搭建,首先得初始化项目 package.json 并安装 jest:

    $ npm init
    $ npm install jest -D
    

    jest 默认不支持 es6,需要使用 babel 来支持 es6,安装 babel:

    $ npm install @babel/core @babel/preset-env -D
    

    配置 babel,修改 .babelrc 文件

    {
        "presets": [
            ["@babel/preset-env", {
                "targets": {
                    "node": "current"
                }
            }]
        ]
    }
    

    2.2 jest 初始化

    安装完 jest 以后,初始化 jest 并根据需要修改根目录下生成的配置文件 jest.config.js

    $ npx jest --init
    

    修改 package.json 中的 scripts,添加执行指令

    (1) 普通执行测试,即 $ npx jest

    {
    	"test": "jest"
    }
    

    (2) 生成覆盖率报告,即 $ npx jest —coverage

    {
    	"coverage": "jest --coverage"
    }
    

    (3) 持续监听变化,默认 o 模式

    {
    	"test": "jest --watch"
    }
    

    (4) 持续监听所有文件变化

    {
    	"test": "jest --watchAll"
    }
    

    2.3 编写测试用例

    先写一个功能模块 init.js

    function add (a, b) { return a + b; }
    function minus (a, b) { return a - b; }
    function multi (a, b) { return a * b; }
    module.exports = { add, minus, multi }
    

    编写对应测试用例文件 init.test.js

    const math = require('./init.js');
    const {add, minus, multi} = math;
    // 断言
    test('测试加法 3 + 7', () => { expect(add(3, 7)).toBe(10); })
    test('测试减法 3 - 3', () => { expect(minus(3, 3)).toBe(0); })
    test('测试乘法 3 * 3', () => { expect(multi(3, 3)).toBe(9); })
    

    执行测试,并在控制台观察结果

    $ npm run test
    $ npm run coverage
    

    3. vue-cli 中使用 jest

    现实项目中,往往不会从零搭建 jest 项目,更多的情况是,需要在一个脚手架已经搭建好的项目中引入自动化测试,此处在 vue-cli 基础上修改 jest 配置,安装好 jest 后需要修改项目根目录下的配置文件 jest.config.js,重点关注 testMatchtestPathIgnorePatterns 两个属性,testMatch 指定了匹配的测试用例文件的路径,而 testPathIgnorePatterns 则可以忽略指定文件,因此使用两个属性可以精确匹配到项目中所有的测试用例。

    module.exports = {
      ...
      testMatch: [
        '**/__tests__/**/*.(js|jsx|ts|tsx)'
      ],
      testPathIgnorePatterns: [
        '.eslintrc.js'
      ],
      testURL: 'http://localhost/'
    }
    

    最后还需要在 package.json 中添加测试指令

    {
    	"test:unit": "vue-cli-service test:unit --watch"
    }
    

    执行对应指令即可在项目中执行测试

    $ npm run test:unit
    

    展开全文
  • 前端自动化测试探索

    2016-10-02 22:19:07
    原文出处: FEX - zhangtao(@设计无极限的地盘)  背景 测试是完善的研发体系中不可或缺的一环。前端同样需要测试,你的css改动...本文试图探讨前端自动化测试领域的工具和实践。 为什么需要自动化测试
  • 前端自动化测试(一)

    千次阅读 2019-09-09 17:11:16
    因为可能项目开发周期短根本没有机会写,所以你没有办法体会到前端自动化测试的重要性。 来说说为什么前端自动化测试如此重要! 先看看前端常见的问题: 修改某个模块功能时,其它模块也受影响,很难快速定位bug ...

空空如也

1 2 3 4 5 ... 20
收藏数 2,801
精华内容 1,120
关键字:

前端自动化测试