阅读 215

前端er应该知道的单元测试--语法简介篇

1.单元测试前置介绍

1.1单元测试的目的

  • 可以保证代码执行的结果和预期保持一致
  • 可以提高开发者编写代码的质量,一般易于测试的代码可读性比较高
  • 如果依赖的组件修改,通过单元测试可以测试出错误

1.2测试类型

  • 单元测试:指原件单元为单位,为软件进行测试。单元可以为一个函数,也可以为一个组件或者一个模块,基本特征为只要出入不变,输出的结果保持不变。一个软件越容易编写单元测试,说明它的模结构化越好,模块之间的耦合性越弱。结合上面这几点,可以看出React的组件化和模块化,很容易编写单元测试;
  • 功能测试:相当于黑盒测试,不用考虑软件代码的具体逻辑和编写规则,从用户的角度去考虑,程序的输入、输出和功能是否符合预期的结果的测试;
  • 集成测试:在单元测试的基础上,将所有的组件按照设计的要求组装成子系统或者系统的测试;
  • 冒烟测试:在正式全面测试之前,对主要功能进行测试,确认主要功能是否满足需求,软件是否能正常运行的测试;

1.3开发模式

TDD(Testing Driven Development):测试驱动开发,强调的是一种开发方式,一测试来驱动整个项目的开发,既先根据需求完成测试用例的编写,在完成功能之前要不断的进行测试,最终目的是通过测试;

BDD(Behavior Driven Development): 行为驱动测试,强调的是编写测试的风格,既测试写的要向自然语言,要让项目法人各个的成员都能看懂的测试;

TDD与BDD的开发模式区别joshldavis.com/2013/05/27/…

2.技术选型 Jest + Enzyme

2.1为什么选择Jest + Enzyme

Jest

Jest 是React官方推荐的测试库,已经被集成到create-react-app中,成为默认的测试库, 同时提供了window对象;

  1. 易用性:基于Jasmine,提供断言库,支持多种测试风格,开箱即用;
  2. 适应性:Jest是模块化、可扩展和可配置的;
  3. 沙箱和快照:Jest内置了JSDOM,能够模拟浏览器环境,并且并行执行;
  4. 快照测试:Jest能够对React组件树进行序列化,生成对应的字符串快照,通过比较字符串提供高性能的UI检测;
  5. Mock系统:Jest实现了一个强大的Mock系统,支持自动和手动mock
  6. 支持异步代码测试:支持Promise和async/await
  7. 自动生成静态分析结果:内置Istanbul,测试代码覆盖率,并生成对应的报告
  8. 社区活跃:Airbnb在Jest基础上进行二次封装的Enzyme;

Enzyme

Enzyme是Airbnb在Jest基础上开源的测试库,提供了一套简洁的强大的API,并内置Cheerio,从而实现了jQuery风格的DOM处理方式,开发体验十分友好。在开源社区有超高人气,同时也获得了React 官方的推荐。

3.测试框架API介绍

3.1 Enzyme有3种渲染方式:render、mount、shallow

  1. Render采用的是第三方库Cheerio的渲染,渲染结果是普通的html结构,对于snapshot使用render比较合适。
  2. Mount: 渲染子组件,同时包含生命周期函数如componentDidMount
  3. Render: 渲染子组件,但不会包含生命周期,同时可用的API也会减少比如setState()

shallow和mount对组件的渲染结果不是html的dom树,而是react树,如果chrome装了react devtool插件,他的渲染结果就是react devtool tab下查看的组件结构,而render函数的结果是element tab下查看的结果。
这些只是渲染结果上的差别,更大的差别是shallow和mount的结果是个被封装的ReactWrapper,可以进行多种操作,譬如find()、parents()、children()等选择器进行元素查找;state()、props()进行数据查找,setState()、setprops()操作数据;simulate()模拟事件触发。
shallow只渲染当前组件,只能能对当前组件做断言;mount会渲染当前组件以及所有子组件,对所有子组件也可以做上述操作。

3.2 组件生命周期测试

  1. componentWillMount
  2. componentDidMount
  3. componentWillReceiveProps
  4. shouldComponentUpdate
  5. componentWillUpdate
  6. componentDidUpdate
  7. componentWillUnmount

其中1、2、7都会在组件渲染及销毁时自动执行,通过断言expect(wrapper.state().XXX)即可测试。当然也可以通过enzyme的mount()和unmount接口手动调用。 其余的在做组件相应的操作就会触发(和在真实环境中执行顺序以及条件一直)

3.3 state测试

wrapper.state()可以获取当前根组件的全部state, 通过wrapper.setState() 可以修改对应的state值

  it('state', () => {
  const wrapper = mount(<Component />);
    expect(wrapper.state().visible).toBe(false);
    wrapper.setState({ visible: true });
    expect(wrapper.state().visible).toBe(true);
  });

复制代码

3.4 props测试

wrapper.props()可以获取当前根组件的全部props, 获取单个的值也可以通过wrapper.prop(key) 通过wrapper.setProps() 可以修改对应的state值

  it('props test', () => {
    const props = { age: 20, text: 'abc' };
    const wrapper = mount(<Component {...props} />);
    // 只有mount渲染才获取会能到props
    // .props() 获取全部的props; .prop[ars] 获取单一的属性
    expect(wrapper.prop('text')).toBe('abc');
    expect(wrapper.props()).toEqual(props);
    wrapper.setProps({ text: 'cba' });
    expect(wrapper.props().text).toBe('cba');
  });

复制代码

3.5 DOM测试

jest 配合enzyme,enzyme可以在jsDom里渲染出虚拟dom,然后我们可以操作它,进行交互测试。依旧有window、document等对象,但是无法往这个dom中插入script标签进行其他资源文件的加载。

  it('click test', () => {
    const wrapper = mount(<Component {...props} />);
    expect(wrapper.find('Button')).toHaveLength(2);
    expect(wrapper.find('Button').at(0).text()).toBe('新 增');
    expect(wrapper.find('Button').at(1).text()).toBe('删 除');
  });
复制代码

Enzyme常用的api

  1. simulate(event, mock):模拟事件,用来触发事件,event为事件名称,mock为一个event object
  2. instance():返回组件的实例
  3. find(selector):根据选择器查找节点,selector可以是CSS中的选择器,或者是组件的构造函数,组件的display name等
  4. at(index):返回一个渲染过的对象
  5. get(index):返回一个react node,要测试它,需要重新渲染
  6. contains(nodeOrNodes):当前对象是否包含参数重点 node,参数类型为react对象或对象数组
  7. text():返回当前组件的文本内容
  8. html(): 返回当前组件的HTML代码形式
  9. props():返回根组件的所有属性
  10. prop(key):返回根组件的指定属性
  11. state():返回根组件的状态
  12. setState(nextState):设置根组件的state
  13. setProps(nextProps):设置根组件的props

3.6 交互测试(DOM事件)

主要利用simulate()接口模拟事件,实际上simulate是通过触发事件绑定函数,来模拟事件的触发。触发事件后,去判断props上特定函数是否被调用,传参是否正确;组件状态是否发生预料之中的修改;某个dom节点是否存在是否符合期望。

  it('click test', () => {
    const wrapper = mount(<Component {...props} />);
    wrapper.find('Button').at(0).simulate('click');
    expect(wrapper.state().list).toHaveLength(4);
  });
复制代码

3.7 异步请求测试

异步代码测试,主要是要告诉测试框架测试何时完成,让其在恰当的时间断言;Jest也提供了多种异步测试的方式:

Promise控制异步代码,可以在.then的回调中进行断言,并且要在该用例中返回Promise对象
describe('Promise async', () => {
  const asyncFunc = (num) => {
    return new Promise((resolve) => {
      setTimeout(() => resolve(num), 5000);
    });
  };
  it('sum1', () => {
    return asyncFunc(2).then((res) => {
      expect(res).toBe(2);
    });
  });
});

复制代码

async/await 语法测试异步代码

describe('async/await', () => {
  const asyncFunc = (num) => {
    return new Promise((resolve) => {
      setTimeout(() => resolve(num), 5000);
    });
  };

  it('async', async () => {
    await expect(asyncFunc(2)).resolves.toBe(2);
  });
});

复制代码

3.8 Snapshot组件快照

借用react-test-renderer库的renderer获得react组件渲染成的React树,调用toJSON接口格式化,在使用jest的expect(tree).toMatchSnapshot()将快照与上一次的快照作对比,首次生成的某个测试案例的快照将会被保存下来,以后每次运行时都会与上一次对比,如果发现不匹配会抛出错误,需要自己去查看差异是否是合法的需要更新的内容。 这种方式对比react组件渲染后的内容,在静态ui上,非常高效的找出差别,维护稳定性。

  it('click test', () => {
    const wrapper = mount(<Component {...props} />);
    expect(wrapper).toMatchSnapshot()
  });
复制代码

运行结束后会在对应的测试脚本的目录生成快照的静态文件

3.8 覆盖率测试

%stmts是语句覆盖率(statement coverage):是不是每个语句都执行了

%Branch分支覆盖率(branch coverage):是不是每个if代码块都执行了

%Funcs函数覆盖率(function coverage):是不是每个函数都调用了

%Lines行覆盖率(line coverage):是不是每一行都执行了

通过jest --coverage命令可以查看当前项目的文件测试情况。运行完成之后项目的根目录会出现coverage文件夹

点击进去对应的文件夹可以看到详细的测试情况

4. 总结

4.1 常用的断言

常用的断言类型介绍

  1. expect(value):要测试一个值进行断言的时候,要使用expect对值进行包裹
  2. toBe(value):使用Object.is来进行比较,如果进行浮点数的比较,要使用toBeCloseTo
  3. not:用来取反
  4. toEqual(value):用于对象的深比较
  5. toMatch(regexpOrString):用来检查字符串是否匹配,可以是正则表达式或者字符串
  6. toContain(item):用来判断item是否在一个数组中,也可以用于字符串的判断
  7. toBeNull(value):只匹配null
  8. toBeUndefined(value):只匹配undefined
  9. toBeDefined(value):与toBeUndefined相反
  10. toBeTruthy(value):匹配任何使if语句为真的值
  11. toBeFalsy(value):匹配任何使if语句为假的值
  12. toBeGreaterThan(number): 大于
  13. toBeGreaterThanOrEqual(number):大于等于
  14. toBeLessThan(number):小于
  15. toBeLessThanOrEqual(number):小于等于
  16. toBeInstanceOf(class):判断是不是class的实例
  17. anything(value):匹配除了null和undefined以外的所有值
  18. resolves:用来取出promise为fulfilled时包裹的值,支持链式调用
  19. rejects:用来取出promise为rejected时包裹的值,支持链式调用
  20. toHaveBeenCalled():用来判断mock function是否被调用过
  21. toHaveBeenCalledTimes(number):用来判断mock function被调用的次数
  22. assertions(number):验证在一个测试用例中有number个断言被调用
  23. extend(matchers):自定义一些断言

更多断言可参考jestjs.io/docs/zh-Han…;

4.2 测试用例的生命周期

beforeAll(fn,timeout): 所有的用例测试运行前执行;

afterAll(fn,timeout): 所有的用例测试运行完后执行;

afterEach(fn): 每个用例测试执行前;

beforeEach(fn): 每个用例测试执行后;

全局和describe都有beforeAll、afterAll、afterEach、beforeEach四个生命周期函数,其中describe中的生命周期运行优先级要低于全局;


beforeAll(() => {
  console.log('globals beforeAll');
});

beforeEach(() => {
  console.log('globals beforeEach');
});

afterEach(() => {
  console.log('globals afterEach');
});

afterAll(() => {
  console.log('globals afterAll');
});

describe('test life cycle', () => {
  beforeAll(() => {
    console.log('describe beforeAll');
  });

    beforeEach(() => {
    console.log('describe beforeEach');
  });

  afterEach(() => {
    console.log('describe afterEach');
  });


  afterAll(() => {
    console.log('describe afterAll');
  });

  it('sum1', () => {
    expect(2 + 3).toEqual(5);
  });
  it('sum2', () => {
    expect(3 + 3).toEqual(6);
  });
});

复制代码

执行结果为:

    globals beforeAll

    describe beforeAll

    globals beforeEach

    describe beforeEach

    describe afterEach

    globals afterEach

    globals beforeEach

    describe beforeEach

    describe afterEach

    globals afterEach

    describe afterAll

    globals afterAll

复制代码
关注下面的标签,发现更多相似文章
评论