zoukankan      html  css  js  c++  java
  • 前端单元测试

    前端单元测试概述

    前端测试工具

    测试分为e2e测试和单元测试和集成测试

    e2e:端到端的测试,主要是测业务,绝大部分情况是指在浏览器上对某一个网站的某一个功能进行操作。

    单元测试工具:mache、ava、jest、jasmine等

    断言库: shoud.js.chai.js 等

    测试覆盖率工具:istanbul

    react 采用jest加enzyne的写法 e2e 测试pupertear

    vue 采用jest e2e 适应nightwatch 的方案

    测试分为三个种类

    • 单元测试

      在计算机编程中,单元测试(英语:Unit Testing)又称为模块测试, 是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法

    • 集成测试

      集成测试,也叫组装测试或联合测试。在单元测试的基础上,将所有模块按照设计要求(如根据结构图)组装成为子系统或系统,进行集成测试。

    • 功能测试

      功能测试就是对产品的各功能进行验证,根据功能测试用例,逐项测试,检查产品是否达到用户要求的功能。

    单元测试和集成测试简单对比

    React & Redux 应用构建在三个基本的构建块上:actions、reducers 和 components。是独立测试它们(单元测试),还是一起测试(集成测试)取决于你。集成测试会覆盖到整个功能,可以把它想成一个黑盒子,而单元测试专注于特定的构建块。从我的经验来看,集成测试非常适用于容易增长但相对简单的应用。另一方面,单元测试更适用于逻辑复杂的应用。尽管大多数应用都适合第一种情况,但我将从单元测试开始更好地解释应用层。

    为何选用jest

    1. 方便的异步测试
    2. snapshot功能(快照测试)
    3. 集成断言库,不许要引用其他第三方库
    4. 对React天生支持
    5. 零配置
    6. 内置代码覆盖率
    7. 强大的Mocks

    Jest 安装与配置

    vue中直接选就可以

    在其他的项目中,直接测试就可以

    npm install --save-dev jest
    

    在package.json中添加

    // 添加测试命令
    {
      "scripts": {
        "test": "jest"
      }
    }
    

    执行命令

    npm test
    

    Jest 的测试脚本名形如.test.js,不论 Jest 是全局运行还是通过npm test运行,它都会执行当前目录下所有的.test.js 或 *.spec.js 文件、完成测试

    Jest 的api与概念

    匹配器(Matchers)

    1、相等匹配

    expact(2 + 2) 将返回我们期望的结果, toBe 就是一个matcher

    test('two plus two is four', () => {
      expect(2 + 2).toBe(4);
    });
    

    toBe 是测试具体的某一个值,如果需要测试对象,需要用到toEqual,toEqual是通过递归检查对象或数组的每个字段。

    test('object assignment', () => {
      const data = {one: 1};
      data['two'] = 2;
      expect(data).toEqual({one: 1, two: 2});
    });
    

    2、真实性匹配,比如:对象是否为null,集合是否为空等等

    在测试中,您有时需要区分undefined、null和false,但有时希望以不同的方式处理这些问题,Jest帮助你明确您想要什么。比如:

    1. toBeNull 仅当expect返回对象为 null时
    2. toBeUndefined 仅当返回为 undefined
    3. toBeDefined 和上面的刚好相反,对象如果有定义时
    4. toBeTruthy 匹配任何返回结果为true的
    5. toBeFalsy 匹配任何返回结果为false的

    3、数字型匹配

    test('two plus two', () => {
      const value = 2 + 2;
      expect(value).toBeGreaterThan(3);
      expect(value).toBeGreaterThanOrEqual(3.5);
      expect(value).toBeLessThan(5);
      expect(value).toBeLessThanOrEqual(4.5);
    
      // toBe and toEqual are equivalent for numbers
      expect(value).toBe(4);
      expect(value).toEqual(4);
    });
    

    于float类型的浮点数计算的时候,需要使用toBeCloseTo而不是 toEqual ,因为避免细微的四舍五入引起额外的问题

    4、字符型匹配 toMatch 匹配规则,支持正则表达式匹配

    test('there is no I in team', () => {
      expect('team').not.toMatch(/I/);
    });
    
    test('but there is a "stop" in Christoph', () => {
      expect('Christoph').toMatch(/stop/);
    });
    

    5、数组类型匹配 toContain 检查是否包含

    const shoppingList = [
      'diapers',
      'kleenex',
      'trash bags',
      'paper towels',
      'beer',
    ];
    
    test('the shopping list has beer on it', () => {
      expect(shoppingList).toContain('beer');
    }); 
    

    6、异常匹配 测试function是否会抛出特定的异常信息,可以用 toThrow 规则

    function compileAndroidCode() {
      throw new ConfigError('you are using the wrong JDK');
    }
    
    test('compiling android goes as expected', () => {
      expect(compileAndroidCode).toThrow();
      expect(compileAndroidCode).toThrow(ConfigError);
    
      // You can also use the exact error message or a regexp
      expect(compileAndroidCode).toThrow('you are using the wrong JDK');
      expect(compileAndroidCode).toThrow(/JDK/);
    });
    

    Asynchronous(测试异步代码)

    1、回调函数

    done() 被执行则意味着callback函数被调用

    function fetchData(callback) {
        setTimeout(() => {
          callback('2')
        }, 2000)
      }
    
    
      test('data is 2', done => {
        function callback(data) {
          expect(data).toBe('2');
          done();
        }
        fetchData(callback)
      })
    

    2、promise验证

    assertions(1)代表的是在当前的测试中至少有一个断言是被调用的,否则判定为失败。

    在Jest 20.0.0+ 的版本中你可以使用 .resolves 匹配器在你的expect语句中,Jest将会等待一直到承诺被实现,如果承诺没有被实现,测试将自动失败。
    如果你期望你的承诺是不被实现的,你可以使用 .rejects ,它的原理和 .resolves类似

      function fetchData() {
        return new Promise((resolve, reject) => {
          setTimeout(() => {
            resolve('2')
          }, 2000)
        })
        
      }
        
    
      test('data is 2', () => {
        expect.assertions(1);
        return expect(fetchData()).resolves.toBe('2');
      })
      test('data is 2', () => { 
        expect.assertions(1);
        return expect(fetchData()).rejects.toMatch('error');
      });
    

    3、使用 Async/Await

    function fetchData(num) {
        return new Promise((resolve, reject) => {
          setTimeout(() => {
            if(num) {
              reject('error')
            } else {
              resolve('2')
            }
          }, 2000)
        })
        
      }
    
    
      test('data is 2', () => {
        expect.assertions(1);
        return expect(fetchData()).resolves.toBe('2');
      })
    
    
      test('the data is 2', async () => {
        expect.assertions(1);
        const data = await fetchData();
        expect(data).toBe('2');
      });
      
      test('the fetch fails with an error', async () => {
        expect.assertions(1);
        try {
          await fetchData(1);
        } catch (e) {
          expect(e).toMatch('error');
        }
      });
    

    当然你也可以将Async Await和 .resolves .rejects 结合起来(Jest 20.0.0+ 的版本)

    test('the data is peanut butter', async () => {
      expect.assertions(1);
      await expect(fetchData()).resolves.toBe('peanut butter');
    });
    
    test('the fetch fails with an error', async () => {
      expect.assertions(1);
      await expect(fetchData()).rejects.toMatch('error');
    });
    

    Mock Functions(模拟器)

    在写单元测试的时候有一个最重要的步骤就是Mock,我们通常会根据接口来Mock接口的实现,比如你要测试某个class中的某个方法,而这个方法又依赖了外部的一些接口的实现,从单元测试的角度来说我只关心我测试的方法的内部逻辑,我并不关注与当前class本身依赖的实现,所以我们通常会Mock掉依赖接口的返回,因为我们的测试重点在于特定的方法,所以在Jest中同样提供了Mock的功能

    Jest中有两种方式的Mock Function,一种是利用Jest提供的Mock Function创建,另外一种是手动创建来覆写本身的依赖实现。

    1、 jest.fn() 方式

    every

    function every(array, predicate) {
        let index = -1
        const length = array == null ? 0 : array.length
      
        while (++index < length) {
          if (!predicate(array[index], index, array)) {
            return false
          }
        }
        return true
      }
      
      module.exports = every
    

    foreach

    function foreach(arr, fn) {
        for(let i = 0, len = arr.length;  i < len; i++) {
            fn(arr[i]);
        }
    }
    
    module.exports = foreach;
    
    const foreach = require('./foreach');
    const every = require('./every');
    
    describe('mock test', () => {
        it('test foreach use mock', () => {
            
            // 通过jest.fn() 生成一个mock函数
            const fn = jest.fn();
    
            foreach([1, 2, 3], fn);
            // 测试mock函数被调用了3次
            expect(fn.mock.calls.length).toBe(3);
           // 测试第二次调用的函数第一个参数是3
            expect(fn.mock.calls[2][0]).toBe(3);
        })
    
        it('test every use mock return value', () => {
            const fn = jest.fn();
            
            // 可以设置返回值
            fn
              .mockReturnValueOnce(true)
              .mockReturnValueOnce(false);
    
    
            const res = every([1, 2, 3, 4], fn);
            expect(fn.mock.calls.length).toBe(2);
            expect(fn.mock.calls[1][1]).toBe(1);
        })
    
        it('test every use mock mockImplementationOnce', () =>{
           // 快速定义mock的函数体,方便测试
            const fn = jest.fn((val, index) => {
                if(index == 2) {
                    return false;
                }
                return true;
            });
    
            const res = every([1, 2, 3, 4], fn);
            expect(fn.mock.calls.length).toBe(3);
            expect(fn.mock.calls[1][1]).toBe(1);
        })
    })
    
    

    2、手动

    假如我的测试文件sum2.js

    function sum2(a, b) {
        if (a > 10) return a * b;
        return a + b;
    }
    

    export default sum2;
    现在如果我们要mock sum2.js 文件的话,需要在sum2.js 同级目录下新建文件夹__mock__,
    然后在此文件下新建文件同名 sum2.js, 只是单纯的返回100

    export default function sum2(a, b) {
        return 100;
    }
    测试用例mock_file.test.js
    
    
    jest.mock('../src/sum2');
    import sum2 from '../src/sum2';
    
    
    it('test mock sum2', () => {
        // 因为此时访问的是__mock__文件夹下的sum2.js 所以测试通过
        expect(sum2(1, 11111)).toBe(100);
    })
    

    手动mock的好处是测试和模拟分离。可以很方便的修改测试用例。如果是复杂的mock建议使用手动新建文件方式

    方便的钩子与全局函数

    1. beforeEach(fn)每一个函数之前
    2. afterEach(fn) 每一个函数之后
    3. beforeAll(fn) 所有的之前
    4. afterAll(fn) 所有的之后
    class Hook {
    
        constructor() {
            this.init();
        }
    
        init() {
            this.a = 1;
            this.b = 1;
        }
    
        sum() {
            return this.a  + this.b;
        }
    }
    
    
    describe('hook', () => {
    
        const hook = new Hook;
    
        // 每个测试用例执行前都会还原数据,所以下面两个测试可以通过。
        beforeEach( () => {
            hook.init();
        })
    
    
        test('test hook 1', () => {
            hook.a = 2;
            hook.b = 2;
            expect(hook.sum()).toBe(4);
        })
    
        test('test hook 2', () => {
    
            expect(hook.sum()).toBe(2);// 测试通过
        })
    })  
    

    describe(name, fn)

    describe(name, fn)创建一个块,在一个“测试套件”中,将几个相关的测试组合在一起

    const myBeverage = {
      delicious: true,
      sour: false,
    };
    
    describe('my beverage', () => {
      test('is delicious', () => {
        expect(myBeverage.delicious).toBeTruthy();
      });
    
      test('is not sour', () => {
        expect(myBeverage.sour).toBeFalsy();
      });
    });
    

    这不是必需的——你可以直接在顶层编写测试块。但是,如果您希望将测试组织成组,那么这就很方便了

    describe.only(name, fn)

    如果你只想运行一次模块测试的话你可以使用 only

    describe.only('my beverage', () => {
      test('is delicious', () => {
        expect(myBeverage.delicious).toBeTruthy();
      });
    
      test('is not sour', () => {
        expect(myBeverage.sour).toBeFalsy();
      });
    });
    
    describe('my other beverage', () => {
      // ... will be skipped
    });
    

    describe.skip(name, fn) describe 等价于 xdescribe

    你可以使用skip 跳过某一个测试

    describe('my beverage', () => {
      test('is delicious', () => {
        expect(myBeverage.delicious).toBeTruthy();
      });
    
      test('is not sour', () => {
        expect(myBeverage.sour).toBeFalsy();
      });
    });
    
    describe.skip('my other beverage', () => {
      // ... will be skipped
    });
    

    使用跳过通常只是一种比较简单的替代方法,如果不想运行则可以暂时将大量的测试注释掉。

    require.requireActual(moduleName)

    返回实际的模块而不是模拟,绕过所有检查模块是否应该接收模拟实现。

    require.requireMock(moduleName)

    返回一个模拟模块,而不是实际的模块,绕过所有检查模块是否正常。

    test(name, fn, timeout) 等价于 it(name, fn, timeout)

    在测试文件中,您所需要的是运行测试的测试方法。例如,假设有一个函数inchesOfRain()应该是零。你的整个测试可以是:

    test('did not rain', () => {
      expect(inchesOfRain()).toBe(0);
    });
    

    第一个参数是测试名称;第二个参数是包含测试期望的函数。第三个参数(可选)是超时(以毫秒为单位),用于指定在中止前等待多长时间。注意:默认的超时是5秒。

    注意:如果测试返回了一个promise,Jest会在测试完成之前等待promise。Jest还将等待,如果你为测试函数提供一个参数,通常称为done。当你想要测试回调时,这将非常方便。请参见如何在此测试异步代码。

    test.only(name, fn, timeout)等同于 it.only(name, fn, timeout) or fit(name, fn, timeout)

    test.skip(name, fn)等同于it.skip(name, fn) or xit(name, fn) or xtest(name, fn)

    当您维护一个大型的代码库时,您可能有时会发现由于某种原因而临时中断的测试。

    如果您想跳过这个测试,但是您不想仅仅删除这个代码,您可以使用skip指定一些测试来跳过。

    test('it is raining', () => {
      expect(inchesOfRain()).toBeGreaterThan(0);
    });
    
    test.skip('it is not snowing', () => {
      expect(inchesOfSnow()).toBe(0);
    });
    

    只有“it is raining”测试运行,因为另一个测试运行test . skip。 您可以简单地对测试进行注释,但是使用skip会更好一些,因为它将保持缩进和语法突出。

    测试覆盖率

    Jest 内置了测试覆盖率工具istanbul,要开启,可以直接在命令中添加 --coverage 参数,或者在 package.json 文件进行更详细的配置。

    搭配React和其它框架的使用 快照功能

    快照测试第一次运行的时候会将被测试ui组件在不同情况下的渲染结果保存一份快照文件。后面每次再运行快照测试时,都会和第一次的比较。

    import React from 'react';
    
    export default class RC extends React.Component {
    
        render() {
            return (
                <div>我是react组件 </div>
            )
        }
    }
    
    import React from 'react';
    import renderer from 'react-test-renderer';
    
    import RC from '../src/react-comp';
    
    test('react-comp snapshot test', () => {
        const component = renderer.create(<RC />);
        //
        let tree = component.toJSON();
        expect(tree).toMatchSnapshot();
    })
    
    test('react-comp snapshot test2', () => {
        const component = renderer.create(<RC />);
        
        let tree = component.toJSON();
        expect(tree).toMatchSnapshot();
    })
    

    执行测试命令,会在test目录下生成一个__snapshots__目录,在此目录下会与一个文件叫snapshot.test.js.snap的快照文件

    迁移

    使用

    npm install -g jest-codemods
    
    
    然后jest-codemods
    

    enzyme API

    shallow() 渲染函数只渲染我们专门测试的组件, 它不会渲染子元素。相反, 用mount()
    1、使用 shallow

    mport { shallow } from 'enzyme';
    
    const wrapper = shallow(<MyComponent />);
    

    我们刚刚可以看到这个测试里用到shallow函数,它支持对DOM的进行结构和事件的响应,如果你对jQuery比较熟悉的话,那么你对它的语法也不会陌生。比如我们测试里用到的find方法,大家经常用它来寻找一些DOM数组。

    简单罗列下它所支持的方法:

    • find(selector) 查找选择器下的DOM 元素,返回一个数组。
    • contains(node) 确定是否包含该节点或者一些节点 ,返回true 或者 false
    • is(selector) 判断改节点是否能够匹配选择器的节点 ,返回true 或者 false
    • hasClass(className) 判断是否包含这个类,返回true 或者 false
    • prop[key] 返回组件上某个属性的值
    • setState(props) 设置组件状态
    • simulate(event[,mock]) 模拟一个节点上的事件

    2、完全DOM渲染

    import { mount } from 'enzyme';
    
    const wrapper = mount(<MyComponent />);
    

    完全DOM渲染主要用于与DOM API进行交互以及需要完整生命周期的组件测试(i.e componentDidMoun)。完全DOM渲染需要DOM 的 API 在全局作用域内。而且需要其运行在近似浏览器的环境里。如果你不想在浏览器里跑这些测试的话,强烈建议你使用mount,一个依赖于jsdom的类库,几乎等同于没有浏览器外壳的浏览器。它也支持了很多方法

    3、静态渲染

    静态渲染,enzyme还提供了静态渲染,将组件渲染成html,用于我们分析html的结构。render相比前两种用法, 主要是在于更换了类库 Cheerio ,而且作者也相信在处理解析上会更好点。

    import { render } from 'enzyme';
    
    const wrapper = render(<MyComponent />);
    

    使用说明

    如果我们在开发过程中就进行了测试(直接采用 TDD 开发模式、或者针对既有的模块写用例),

    会有如下的好处:

    • 保障代码质量和功能的实现的完整度
    • 提升开发效率,在开发过程中进行测试能让我们提前发现 bug ,此时进行问题定位和修复的速度自然比开发完再被叫去修 bug 要快许多
    • 便于项目维护,后续任何代码更新也必须跑通测试用例,即使进行重构或开发人员发生变化也能保障预期功能的实现

    当然,凡事都有两面性,好处虽然明显,却并不是所有的项目都值得引入测试框架,毕竟维护测试用例也是需要成本的。对于一些需求频繁变更、复用性较低的内容,比如活动页面,让开发专门抽出人力来写测试用例确实得不偿失。而那些适合引入测试场景大概有这么几个:

    • 需要长期维护的项目。它们需要测试来保障代码可维护性、功能的稳定性较为稳定的项目、或项目中
    • 较为稳定的部分。给它们写测试用例,维护成本低
    • 被多次复用的部分,比如一些通用组件和库函数。

    因为多处复用,更要保障质量

  • 相关阅读:
    Pandas 包基础
    记录numpy 数组打印形式
    WordPress 模板层次结构
    WordPress 主题开发
    WordPress 主题开发
    WordPress 主题开发
    WordPress 主题开发
    WordPress 主题开发
    WordPress 主题开发
    WordPress 主题开发
  • 原文地址:https://www.cnblogs.com/chenjinxinlove/p/9215173.html
Copyright © 2011-2022 走看看