前端单元测试

前端单元测试概述

前端测试工具

测试分为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 要快许多
  • 便于项目维护,后续任何代码更新也必须跑通测试用例,即使进行重构或开发人员发生变化也能保障预期功能的实现

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

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

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

posted @ 2018-06-22 19:08  快乐~  阅读(878)  评论(0编辑  收藏  举报