单元测试

单元测试就是测试最小单元,我们的单元可能是一个函数,一个button的样式,一个文案等等都可能是一个单元

应用范围

  1. 通用工具库
  2. 公共函数
  3. 公共组件

可有可无

  • 写单测比较费时,有这个时间不如多做几个需求
  • 单纯对于一个结果的输入输出来说,很多时候浏览器给我们的信息更直观也更容易发现问题
  • 前端很少需要提供接口给其他人

意义所在

  • 提升代码质量,快速定位bug,减少bug
  • 保证代码的整洁清晰,让别人接手维护更方便
  • 规范组织结构
  • 兼容各种边界条件,降低出错率

TTD

TDD (test-driven development) - 测试驱动开发

1、Add a test

在测试驱动开发中,每个新特性都以编写测试开始。编写一个测试,它定义了一个函数或一个函数的改进,这应该是非常简洁的。要编写测试,开发人员必须清楚地了解该特性的规范和要求。开发人员可以通过use cases和user stories来完成这个任务,以覆盖需求和异常条件,并且可以在任何适合于软件环境的测试框架中编写测试。它可以是现有测试的修改版本。这是测试驱动开发与编写代码后编写单元测试的一个区别特性:它使开发人员在编写代码之前关注需求,这是一个微妙但重要的区别。

2、重构

在我们维护代码的过程中,必须定期清理不断增长的代码。新代码可以从方便的地方转移到它更符合逻辑的地方。重复的代码必须被删除。对象、类、模块、变量和方法名应该清楚地表示它们当前的用途,因为添加了额外的功能,会导致很难去分辨命名的意义。等到代码不断的增加会使得规范的命名和删除重复代码越来越收益。

3、开发风格转换

测试代码应该在功能代码之前去写,这是被认为有更多的好处。它能确定这个程序是为可测试而写的,因为开发者需要从一开始就必须考虑如何编写测试程序,而不是后面再去考虑。此外,写测试代码能够更深的理解需求。

unit 测试

单元测试是程序员写好自己的逻辑后可以很容易的测试自己的逻辑返回的是不是都正确

Jest

更多细节可查阅 @vue/cli-plugin-unit-jest

函数

对于一些基本带有返回的函数,我们一般可以直接通过断言它的返回值

// function add(num){ return num + 1}
expect(add(1)).toBe(2);

如果一个函数里面并没有返回,而是调用了一个回调函数,我们可以通过模拟函数来判断它是否如期调用就可以了

function forEach(items, callback) {
  for (let index = 0; index < items.length; index++) {
    callback(items[index]);
  }
}

const mockCallback = jest.fn();
forEach([0, 1], mockCallback);

// 被调用
expect(mockCallback).toBeCalled();

// 被调用了两次
expect(mockCallback).toBeCalledTimes(2);

// 被调用时传入的参数是0
expect(mockCallback).toHaveBeenCalledWith(0);

异步的请求也可以看作是一个函数,可以用jest.mock的方法模拟请求进行测试。

组件

React中,我们测试的目的一般都是为了测试是否渲染了正确的DOM结构和业务逻辑。

公共组件一般是一些无状态的纯函数组件,测起来也相对简单

// 通过enzyme创建一个虚拟的组件
const wrapper = shallow(
    <wrapperComponent />/
);
// 通过class观察组件是否成功渲染
expect(wrapper.is('.wrapper-class')).to.equal(true);

当然,有些组件我们还有通过props传入一些属性;state和一些方法;甚至一些生命周期

class wrapperComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      number: props.number
    }
  }
  
    componentDidMount() {
    console.log(this.state.number)
  }
  
  handleClick = () => {
    let { number } = this.state;
    this.setState({
      number: number + 1
    });
  }

  render() {
    return (
      <div className="wrapper-class">
        <button onClick={this.handleClick}>+</button>
      </div>
    )
  }
}

const wrapper = shallow(
    <wrapperComponent number={0}/>/
);

// 测试props
expect(wrapper.props()).toHaveProperty('number',0);
// 测试生命周期
expect(wrapper.prototype.componentDidMount.calledOnce).toBe(true);
// 测试方法是否实现
wrapper.instance().handleClick();
expect(wrapper.state()).to.deep.equal({number: 1});

值得注意的是,组件内部嵌入了自组件也会增加我们的测试复杂度,因为shallow只做了浅层渲染,在考虑我们要做自组件测试的时候,应该采用深度渲染获取子组件,例如mount方法。shallow和mount的使用会影响事件的触发不同

高阶组件

React中你可能会涉及到高阶组件(High-Order Component),理解高阶组件,我们可以把High-Order 和 Component分开理解。高阶组件可以看作一个组件包含了另一组件,我们如果把外层的组件看作High-Order,里面包裹的组件看作普通的Component就好理解一些。

那么测试的时候,我们也可以把他们分开来写。

// 高阶组件 component.js
export function HocWrapper(WrapprComponent) {
  return class Hoc extends React.Component {
    state = {
      loading: false
    };
    render() {
      return <WrapperComponent {...this.state} {...this.props} />;
    }
  };
}

export class WrapprComponent extends React.Component {
  render() {
    return <div>hello world</div>;
  }
}

//component.test.js
import { HocWrapper, WrapprComponent } from "./component.js";
const wrapper = mount(HocWrapper(WrapprComponent));
// 测试有loading属性
expect(wrapper.find('WrapperComp').props()).toHaveProperty('loading');

一般来说,为了测试,我们要文件里吧HocWrapper函数和我们的WrapprComponent组件分别都export出来,当然我们自己写的高阶组件都会这样做。

而我们在开发中会用到诸如Redux的connect,这也是一种高阶组件的形式,所以这时候为了测试,我们会在一个文件中export一个没有connect的组件作为测试组件,props作为属性传递进去。

状态管理

React中我们一般用Redux做状态管理,分为action,reducer,还会有saga做副作用的处理。

对于actions的测试,我们主要验证每个action对象是否正确(其实我觉得这个用TS做类型推导就相当于加了测试)

// action是否返回正确类型
expect(actions.add(1)).toEqual({type: "ADD", payload: 1});

reducer就是一个纯函数,而且每个action对应的reducer职责也比较单一,所以可以作为公共函数去做测试。我们主要测试的内容也是看是否可以根据action的type返回正确的状态。

reducer测试的边界条件一般是我们初始化的store,如果没有action匹配,就返回默认的store。

import { reducer, defaultStore } from './reducers';
const expectedState= {number: 1}
// 根据action是否返回期望的store
expect(reducer(defaultStore, {type: "ADD",1})).toEqual(expectedState);
// 测试边界条件
expect(reducer(defaultStore, {type: "UNDEFINED",1})).toEqual(defaultStore);

如果你用了redux,可能还会用一些库来创建并记录store里的衍生数据,组成我们常用的selector函数,我们测试的重点放在是否能组成新的selector,并且它是根据store的变化而变化。

// selectors.js
export const domainSelector = (store) => store.init;
export const getAddNumber = createSelector(
  domainSelector,
  (store) => {number: store.number + 1},
);

// selectors.test.js
import { getAddNumber } form './selectors'
import { reducer, defaultStore } from './reducers';
// 判断生成selector
expect(getAddNumber(store)).toEqual({number: 1});
// 判断改变store生成新的selector
reducer(defaultStore, {type: "ADD",1})
expect(getAddNumber(store)).toEqual({number: 2});
 

对于一些请求和异步的操作,我们可能用到了saga来管理。saga对于异步我们会分为正常运行和捕获错误去进行测试。

// saga.js
function* callApi(url) {
  try {
    const result = yield call(myApi, url);
    yield put(success(result.json()));
    return result.status;
  } catch (e) {
    yield put(error(e));
    return -1;
  }
}

// saga.test.js
// try
const gen = cloneableGenerator(fetchProduct)();
const clone = gen.clone();
const url = "http://test.com";
expect(clone.next().value).toEqual(call(myApi, url));
expect(clone.next().value).toEqual(put({ type: 'SUCCESS', payload: 1 }));

// catch 要跳到catch,就要让它错误
const error = 'not found';
const clone = gen.clone();
// 需要执行
clone.next();
expect(gen.throw('not found').value).toEqual(put({ type: 'ERROR', error }));

Mocha

(配合 mocha-webpack)

更多细节可查阅 @vue/cli-plugin-unit-mocha

E2E 测试

端到端测试是测试所有的需求是不是都可以正确的完成,而且最终要的是在代码重构,js改动很多之后,需要对需求进行测试的时候测试代码是不需要改变的

Cypress

更多细节可查阅 @vue/cli-plugin-e2e-cypress

Nightwatch

更多细节可查阅 @vue/cli-plugin-e2e-nightwatch

参考网址

https://www.jianshu.com/p/d99dcc4d9448
https://www.jianshu.com/p/f0d9abd29814?from=singlemessage
上一篇:shell expect 拷贝文件夹有问题


下一篇:Mac配置jdk以及maven