点餐前端测试最佳实践

做者介绍:柯培霖,美团点评工程师。前端

2019 年前端测试依然是一个煊赫一时的话题。笔者在今年 5 月份参加 Vueconf 的时候,Vue 单元测试的主题演讲者曾向现场的参与者发出提问,有多少团队引入了单元测试,意外的是只有寥寥数人举起了手。尽管,那个时候笔者的团队也尚未引入前端测试,可是考虑到测试的必要性,且团队正在着手一个新项目,因此回去以后在这个新项目全量地接入了前端测试。vue

现现在大部分互联网团队都是走 敏捷开发 的节奏。实际上,自动化测试才是实现“敏捷”的基本保障。业务端的快速上线和快速验证对技术侧的响应力提出了更高的要求:更快上线,持续上线。再考虑到人员流动和应用逐步变大的事实,往后迭代的成本只会变得愈来愈高。固然这个项目迭代的成本也跟项目的复杂度有关,好比笔者所在的点餐业务,项目有足够的复杂性,有些细微的改动点其实会牵扯到不少内容,而对刚加入团队的新人就会显得不太友好。所以,项目拥有前端测试是必不可少的,它可以有效保障业务迭代的质量和稳定性。node

什么是前端测试?

咱们常常说的单元测试其实只是前端测试的一种。前端测试分为单元测试,UI 测试,集成测试和端到端测试。react

  • 单元测试:是指对软件中的最小可测试单元进行检查和验证,一般指的是独立测试单个函数。
  • UI 测试:是对图形交互界面的测试。
  • 集成测试:就是测试应用中不一样模块如何集成,如何一块儿工做,这和它的名字一致。
  • 端到端测试(e2e):是站在用户角度的测试,把咱们的程序当作是一个黑盒子,我不懂你内部是怎么实现的,我只负责打开浏览器,把测试内容在页面上输入一遍,看是否是我想要获得的结果。

技术选型

前端测试的框架可谓是百花齐放。ios

  • 单元测试有 Mocha, Ava, Karma, Jest, Jasmine 等。
  • UI 测试有 ReactTestUtils, Test Render, Enzyme, React-Testing-Library, Vue-Test-Utils 等。
  • e2e 测试有 Nightwatch, Cypress, Phantomjs, Puppeteer 等。

由于咱们的项目使用的是 React 技术栈,这里主要介绍 React 项目的技术选型和使用。shell

单元测试

框架 断言 仿真 快照 异步测试 环境 并发测试 测试覆盖率
Mocha 默认不支持,可配置 默认不支持,可配置 默认不支持,可配置 友好 全局环境 需配置
Ava 默认支持 不支持,需第三方配置 默认支持 友好 隔离环境 不支持,需第三方配置
Jasmine 默认支持 默认支持 默认支持 不友好 全局环境 不需配置
Jest 默认支持 默认支持 默认支持 友好 隔离环境 不需配置
Karma 不支持,需第三方配置 不支持,需第三方配置 不支持,需第三方配置 不支持,需第三方配置 - - 需配置
  • Mocha 是生态最好,使用最普遍的单测框架,可是他须要较多的配置来实现它的高扩展性。
  • Ava 是更轻量高效简单的单测框架,可是自身不够稳定,并发运行文件多的时候会撑爆 CPU.
  • Jasmine 是单测框架的“元老”,开箱即用,可是异步测试支持较弱。
  • Jest 基于 Jasmine, 作了大量修改并添加了不少特性,一样开箱即用,但异步测试支持良好。
  • Karma 能在真实的浏览器中测试,强大适配器,可配置其余单测框架,通常会配合 Mocha 或 Jasmine 等一块儿使用。

每一个框架都有本身的优缺点,没有最好的框架,只有最适合的框架。Augular 的默认测试框架就是 Karma + Jasmine,而 React 的默认测试框架是 Jest.npm

Jest 被各类 React 应用推荐和使用。它基于 Jasmine,至今已经作了大量修改并添加了不少特性,一样也是开箱即用,支持断言,仿真,快照等。Create React App 新建的项目就会默认配置 Jest,咱们基本不用作太多改造,就能够直接使用。json

UI 测试

UI 测试尽管有官方的测试框架 ReactTestUtils 和 Test Render,可是它们的 API 比较复杂,官方文档也是推荐使用 react-testing-library 或 Enzyme 这两个库。redux

Note: We recommend using React Testing Library which is designed to enable and encourage writing tests that use your components as the end users do. Alternatively, Airbnb has released a testing utility called Enzyme, which makes it easy to assert, manipulate, and traverse your React Components’ output.axios

React Testing Library 和 Enzyme 都是基于 ReactTestUtils 和 Test Render,封装了更简洁易用的 API。

Enzyme 出来的更早,可是它经常会滞后于 React 功能的实现(大约半年左右,好比不支持 hooks,我不肯定如今是否支持了)。React Testing Library 出的比较晚,但倾向于支持 React 的新功能,这对我来讲在测试 Hooks 时是一个巨大的好处。

Enzyme 是从代码实现的角度出发进行测试,基于 state 和 props,而 React Testing Library 是从用户体验的角度出发,因此是基于 dom 进行测试。它也可能有更好的开发体验,以及更稳定的测试。这种方法使重构变得垂手可得,同时也能够实现可访问性的最佳实践。

固然由于 Enzyme 出的比较早,它的周围生态更好,不少大厂都用了它,不过也有一些正在作 迁移。我但愿可以尝试更新更好的框架,因此最后选择了 React Testing Library.

e2e 测试

框架 是否跨浏览器支持 实现 优势 缺点
Nightwatch Selenium 能够和其余框架一块儿使用。适用局部的功能测试场景 不支持 TypeScript, 社区文化稍弱于其余几个框架
Cypress Chrome 容易调试和日志记录,使用 Mocha 做为它的测试结构,若单测使用的是 Mocha,全部测试使用的是相同的结构,看起来更“标准” 缺少高级功能
Testcafe Testcafe 支持TS,可并行测试,开箱即用 不支持录屏,DOM 快照等高级功能
Puppeteer Chrome 速度快,易调试 Headless Chrome 不支持安装扩展

Puppeteer 是 Google Chrome 团队推出的库,尽管它相对其余 e2e 框架更新,但它一样也有一个庞大的社区。它拥有更简洁易用的 API,更快的运行速度,已逐渐成为业内自动化测试的标杆,俘获大量 Selenium 用户的心。能够看下近年来 e2e 测试框架的 npm trends.

结论

通过分析,最后咱们项目的技术选型为 Jest + React Testing Library + Puppeteer

而对于 Vue 的项目,为了保持技术栈的统一,咱们选用了 Jest + Vue-Test-Utils + Puppeteer

编写原则

  • 测试代码时,只考虑测试,不考虑内部实现
  • 数据尽可能模拟现实,越靠近现实越好
  • 充分考虑数据的边界条件
  • 对重点、复杂、核心代码,重点测试
  • 利用 AOP(beforeEach、afterEach),减小测试代码数量,避免无用功能
  • 测试、功能开发相结合,有利于设计和代码重构

编写说明

将来的项目都是基于 Talos 生成,其实也就是使用了 Create-React-App 生成 React 项目,使用了 Vue-CLI@3 生成了 Vue 项目。自己默认都有 Jest 的配置,不需作大的改动。

  • 单元测试和 UI 测试的文件夹统一命名为 tests,测试文件以 .test.js 为后缀
  • tests 文件夹与它们正在测试的代码放在同级目录下,以便相对路径导入时路径更短
  • e2e 测试的文件夹命名为 e2e,并与 src 同放在根目录下
  • VScode 和 WebStorm 都有对应的 Jest 插件,安装后书写代码时有代码补全,debug 和自动运行等功能

如何编写测试

其实,Jest 的语法蛮简单的,只须要熟悉几个 API 就能够快速上手测试了。

工具类函数的单元测试

// lib/utils.js
export function hexToRGB(hexColor) {
  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hexColor);
  return result ? [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)] : [];
}

// lib/__tests__/utils.test.js
import { hexToRGB } from '../util';

describe('将16进制颜色转为 rgb', () => {
  it('小写', () => {
    expect(hexToRGB('#ffc150')).toEqual([255, 193, 80]);
  });
  it('大写', () => {
    expect(hexToRGB('#FFC150')).toEqual([255, 193, 80]);
  });
});
复制代码

只须要给定函数的输入,以后调用函数,验证它的输出与指望的是否同样。

Redux 的单元测试

测试 Reducer

Reducer 把 action 合并到以前的 state,并返回新的 state。由于项目里使用了 Immutable.js,因此须要使用 merge 操做。

// store/reducers/cart.js
import Immutable from 'immutable';
import { UPDATE_CART_DISH_LIST, UPDATE_CART_DISH_SORT_MAP_LIST } from '../actions/cart';

export const initialState = Immutable.Map({
  cartDishList: Immutable.Map({}),
  cartDishSortMapList: Immutable.List([]),
});

export default (state = initialState, action) => {
  switch (action.type) {
    case UPDATE_CART_DISH_LIST:
      return state.merge({
        cartDishList: action.cartDishList,
      });
    case UPDATE_CART_DISH_SORT_MAP_LIST:
      return state.merge({
        cartDishSortMapList: action.cartDishSortMapList,
      });
    default:
      return state;
  }
};

// store/reducers/__tests__/cart.js
import Immutable from 'immutable';
import cartReducer, { initialState } from '../cart';
import { UPDATE_CART_DISH_LIST, UPDATE_CART_DISH_SORT_MAP_LIST } from '../../actions/cart';

describe('cart reducer', () => {
  it('返回初始化的 state', () => {
    expect(cartReducer(undefined, {})).toEqual(initialState);
  });

  it('更新购物车', () => {
    const state = Immutable.Map({ cartDishList: null });
    const action = { type: UPDATE_CART_DISH_LIST, cartDishList: Immutable.Map({}) };
    const newState = Immutable.Map({ cartDishList: Immutable.Map({}) });
    expect(cartReducer(state, action)).toEqual(newState);
  });

  it('更新购物车菜品顺序', () => {
    const state = Immutable.Map({ cartDishSortMapList: null });
    const action = { type: UPDATE_CART_DISH_SORT_MAP_LIST, cartDishSortMapList: Immutable.List([]) };
    const newState = Immutable.Map({ cartDishSortMapList: Immutable.List([]) });
    expect(cartReducer(state, action)).toEqual(newState);
  });
});
复制代码

测试普通 Action

普通 Action 就是一个返回了普通对象的函数,测试起来至关简单。

// store/actions/cart.js
export const UPDATE_SHOPID = 'UPDATE_SHOPID';

export function updateShopIdAction(shopId) {
  return {
    type: UPDATE_SHOPID,
    shopId,
  };
}

// store/actions/__tests__/cart.test.js
import { updateShopIdAction, UPDATE_SHOPID } from '../cart';

test('更新 shopId', () => {
  const expectedAction = { type: UPDATE_SHOPID, shopId: '111' };
  expect(updateShopIdAction('111')).toEqual(expectedAction);
});
复制代码

测试带中间件的复合 Action

项目里使用了 redux-thunk 这个中间件,咱们须要使用 redux-mock-store 来把中间件应用于模拟的 store.

// store/actions/cart.js
export function updateShopIdAction(shopId) {
  return {
    type: UPDATE_SHOPID,
    shopId,
  };
}
export function updateTableNumAction(tableNum) {
  return {
    type: UPDATE_TABLE_NUM,
    tableNum,
  };
}
export function updateBaseInfo(shopId, tableNum) {
  return (dispatch) => {
    dispatch(updateShopIdAction(shopId));
    dispatch(updateTableNumAction(tableNum));
  };
}

// store/actions/__tests__/cart.test.js
import configureStore from 'redux-mock-store';
import Immutable from 'immutable';
import thunk from 'redux-thunk';
import { updateBaseInfo, UPDATE_SHOPID, UPDATE_TABLE_NUM } from '../cart';

const middlewares = [thunk];
const mockStore = configureStore(middlewares);

test('updateBaseInfo', () => {
  const store = mockStore(Immutable.Map({}));
  store.dispatch(updateBaseInfo('111', '111'));
  const actions = store.getActions();
  const expectPayloads = [{ type: UPDATE_SHOPID, shopId: '111' }, { type: UPDATE_TABLE_NUM, tableNum: '111' }];
  expect(actions).toEqual(expectPayloads);
});
复制代码

测试异步 Action

咱们须要借助 axios-mock-adapter 这个包来模拟请求。Create-React-App 默认会安装这个包。

// store/asyncActions/shop.js
import { loadShopInfoAction } from '../actions/main';
import shop from '../../api/shop';
import Loading from '../../components/common/Loading';

export const getShopInfo = (mtShopId, tableNum) => (dispatch) => {
  Loading.show();
  return shop.getShopInfo({ mtShopId, tableNum })
    .then((res) => {
      Loading.close();
      const result = res.data;
      dispatch(loadShopInfoAction(result));
    })
    .catch((e) => {
      Loading.close();
      console.error(e);
    });
};

// store/asyncActions/__tests__/shop.test.js
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import MockAdapter from 'axios-mock-adapter';
import instance from '@lib/axios';
import { getShopInfo } from '../main';
import { LOAD_SHOP_INFO } from '../../actions/main';

const middlewares = [thunk];
const mockStore = configureStore(middlewares);
const mockHttp = new MockAdapter(instance); // 继承了 axios.js 文件里的配置

test('getShopInfo', () => {
  const store = mockStore({});
  const expectPayloads = [{ type: LOAD_SHOP_INFO, shopInfo: { peopleCount: 0 } }];

  mockHttp.onGet('/shopInfo')
    .reply(200, {
      data: { peopleCount: 0 },
    });

  store.dispatch(getShopInfo())
    .then(() => {
      const actions = store.getActions();
      expect(actions).toEqual(expectPayloads);
    });
});
复制代码

UI 测试

对于单元测试来讲,是成本低且收益高,而对 UI 测试来讲,可能更像是成本高但收益低。 可是对于一些公共组件的测试仍是颇有必要的,就像笔者前文说到过的同样,当项目的代码足够复杂时,一个通用组件的改动迎接你的可能就是一个线上 Case。

函数组件

下面简单的看一个加减菜组件的测试(精简了一部分逻辑)。

import React from 'react';
import Immutable from 'immutable';
import './NumberCount.less';

const NumberCount = (props) => {
  const { dish, count, addToCart, minusDish, minusPoint,} = props;

  return (
    <div className="number-count">
      {
        count > 0
          ? (
            <>
              <div className="minus">
                <span className="minus-icon" />
                <span className="minus-trigger" onClick={() => minusDishToCart(dish)} />
              </div>
              <div className="num">{count}</div>
            </>
          )
          : null
      }
      <div className="plus">
        <span className="plus-icon" />
        <span className="plus-trigger" onClick={() => addToCart(dish)} />
      </div>
    </div>
  );
};

export default NumberCount;
复制代码

对于这个组件来讲,逻辑仍是比较简单的。 咱们的测试点在加菜和减菜按钮的事件是否被正确触发,当数量为 0 时,减号按钮和数量是否展现,数量不为 0 时,展现是否正确。

import React from 'react';
import { render, cleanup, fireEvent } from '@testing-library/react';
import Immutable from 'immutable';
import NumberCount from '../NumberCount';

afterEach(cleanup);

test('点击加菜按钮', () => {
  const props = {
    dish: Immutable.fromJS({ spuId: 111 }),
    count: 0,
    addToCart: jest.fn(),
  };
  const { container } = render(<NumberCount {...props} />);
  const addButton = container.querySelector('.plus-trigger');
  const minusButton = container.querySelector('.minus-trigger');
  const numDiv = container.querySelector('.num');
  expect(minusButton).toBeNull();
  expect(numDiv).toBeNull();
  fireEvent.click(addButton);
  expect(props.addToCart).toBeCalledWith(props.dish);
});

test('点击减菜按钮', () => {
  const props = {
    dish: Immutable.fromJS({ spuId: 111 }),
    count: 1,
    addToCart: jest.fn(),
    minusDish: jest.fn(),
  };
  const { container } = render(<NumberCount {...props} />);
  const minusButton = container.querySelector('.minus-trigger');
  const numDiv = container.querySelector('.num');
  expect(numDiv.innerHTML).toBe('1');
  fireEvent.click(minusButton);
  expect(props.minusDish).toBeCalledWith(props.dish);
});
复制代码

使用 connect 包裹后的高阶组件

尽管理论上 components 里面的公共组件都应该是无状态组件,可是有时候有些公用的组件写成有状态组件可能更容易被使用,开发成本更低。 Redux 官方推荐直接测试 connect 包裹前的组件。

In order to be able to test the App component itself without having to deal with the decorator, we recommend you to also export the undecorated component.

这边给出一个样例。

import React from 'react';
import { connect } from 'react-redux';
import Immutable from 'immutable';
import ImmutableBaseComponent from './ImmutableBaseComponent';
import { selectSkuDish, toggleMultiPanelAction } from '../../store/actions/cart';
import { computeCount } from '@modules/cartHelper';
import './SelectDish.less';

// 须要将这个未被 connect 的组件 export, 用于测试
export class SelectDish extends ImmutableBaseComponent {
  togglePanel = (e, spuDish) => {
    e.stopPropagation();
    this.props.selectSkuDish(spuDish);
    this.props.toggleMultiPanelAction(true);
  }

  render() {
    const { spuDish, cartDishList } = this.props;
    const count = computeCount(spuDish, cartDishList);
    return (
      <div className="select-dish" onClick={e => this.togglePanel(e, spuDish)}> 选择 { count > 0 && <span>{count}</span> } </div>
    );
  }
}

const mapStateToProps = state => ({
  cartDishList: state.getIn(['cart', 'cartDishList']),
});

const mapDispatchToProps = dispatch => ({
  selectSkuDish: spuDish => dispatch(selectSkuDish(spuDish)),
  toggleMultiPanelAction: show => dispatch(toggleMultiPanelAction(show)),
});

export default connect(mapStateToProps, mapDispatchToProps)(SelectDish);
复制代码

能够看到代码中 export 了包裹前的 SelectDish, 这样就能进行以下测试:

import React from 'react';
import { render, cleanup, fireEvent } from '@testing-library/react';
import Immutable from 'immutable';
import { SelectDish } from '../SelectDish';

afterEach(cleanup);

test('选择多规格菜品', () => {
  const props = {
    spuDish: Immutable.fromJS({ spuId: '111' }),
    cartDishList: Immutable.fromJS({}),
    selectSkuDish: jest.fn(),
    toggleMultiPanelAction: jest.fn(),
  };
  const { container } = render(<SelectDish {...props} />); const selectButton = container.querySelector('.select-dish'); fireEvent.click(selectButton); expect(props.selectSkuDish).toBeCalledWith(props.spuDish); expect(props.toggleMultiPanelAction).toBeCalledWith(true); }); 复制代码

编写测试小技巧

在写某些模块的单测或是 UI 测试时,你们可能会发现一些难以测试的点,好比 Localstorage, 或一些延时函数的触发。下面一块儿看一下如何处理这些状况。

LocalStorage

由于 Jest 的环境是基于 jsdom, 因此咱们须要去模拟 localstorage 的行为。借鉴 Vue2.0 里数据侦测的方法。 新建一个文件,加入以下代码

// config/jest/browserMocks.js
const localStorageMock = (function () {
  let store = {};
  return {
    getItem(key) {
      return store[key] || null;
    },
    setItem(key, value) {
      store[key] = value.toString();
    },
    removeItem(key) {
      delete store[key];
    },
    clear() {
      store = {};
    },
  };
}());

Object.defineProperty(window, 'localStorage', {
  value: localStorageMock,
});
复制代码

以后须要在 Jest 的配置中将该文件配置为启动文件

setupFiles: [
  '<rootDir>/config/jest/browserMocks.js',
]
复制代码

延时函数

利用 Jest 提供的 jest.useFakeTimers(),jest.runAllTimers(),jest.useRealTimers() 等 API 来完成测试。 能够看如下 Toast 组件的 UI 测试。

import React from 'react';
import './Toast.less';

class Toast extends React.Component {
  static close() {
    Toast.beforeClose && Toast.beforeClose();
    if (Toast.timer) {
      clearTimeout(Toast.timer);
      Toast.timer = null;
    }
    if (Toast.toastWrap) {
      document.body.removeChild(Toast.toastWrap);
      Toast.toastWrap = null;
    }
  }

  componentDidMount() {
    const { duration, beforeClose } = this.props;

    Toast.beforeClose = beforeClose;
    if (duration > 0) {
      Toast.timer = setTimeout(() => {
        Toast.close();
      }, duration);
    }
  }

  render() {
    if (Toast.toastWrap) Toast.close();
    const { content, hasMask } = this.props;

    return (
      <div className="toast-wrap"> { hasMask && <div className="toast-mask" /> } <div className="toast-box"> {content} </div> </div> ); } } export default Toast; 复制代码

咱们这里就检查的写一点测试,测试 Toast 弹窗内的内容是否一致,beforeClose 事件是不是在弹窗关闭时才触发。

import React from 'react';
import { render, cleanup } from '@testing-library/react';
import Toast from '../Toast';

afterEach(cleanup);

test('正常弹窗', () => {
  jest.useFakeTimers();
  const props = {
    duration: 2000,
    content: 'hello world',
    beforeClose: jest.fn(),
  };
  const { container } = render(<Toast {...props} />); const toastDiv = container.querySelector('.toast-box'); expect(toastDiv.innerHTML).toBe('hello world'); expect(props.beforeClose).not.toBeCalled(); jest.runAllTimers(); expect(props.beforeClose).toBeCalled(); jest.useRealTimers(); }); 复制代码

e2e 测试

对于 e2e 测试来讲,咱们不须要写太多的代码,毕竟咱们都有专业的 QA 同窗。我认为只须要简单的覆盖主流程,好比咱们的点餐业务,从最开始的选择人数页进入菜单页,进行加菜,减菜,再进入下单页下单等。 e2e 还须要对 Jest 作一点配置。新建一个 jest-e2e.config.js 文件,不与单测的配置冲突。

module.exports = {
  preset: 'jest-puppeteer',
  testRegex: 'e2e/.*\\.test\\.js$',
};
复制代码

在 package.json 加一行命令

"scripts": {
  "test:e2e": "jest -c jest-e2e.config.js --detectOpenHandles"
}
复制代码

这边简单贴一点 e2e 的代码。

// e2e/regression.test.js
const puppeteer = require('puppeteer');

const TARGET_URL = 'XXX';
const width = 375;
const height = 667;

test('主流程', async () => {
  const browser = await puppeteer.launch({ headless: false });
  const context = await browser.createIncognitoBrowserContext();
  const page = await context.newPage();
  await page.goto(TARGET_URL, {
    waitUntil: 'networkidle2', // 等到空闲
  });
  await page.setViewport({ width, height });
  await page.waitForSelector('.people-count');
  await page.screenshot({ path: 'e2e/screenshots/main.png' });
  await page.click('.people-count:nth-child(1)');
  await page.click('.start-btn');
  await page.waitFor(3000);
  await page.screenshot({ path: 'e2e/screenshots/menu.png' });

  // 添加普通菜
  await page.click('.meal-list-style > ul > li:nth-child(8) .plus-trigger');

  // 展开购物车
  await page.click('#cart-chef');
  await page.screenshot({ path: 'e2e/screenshots/cart.png' });
  await page.click('#cart-chef');

  // 进入下单页
  await page.click('.cart-bar > .btn.highlight');
  await page.waitFor(3000);
  await page.screenshot({ path: 'e2e/screenshots/order-confirm.png' });

  await browser.close();
}, 20000);
复制代码

测试覆盖率

为项目添加一行命令,就能够查看项目的测试覆盖率。

"scripts": {
  "cov": "node scripts/test.js --coverage"
}
复制代码

编写测试实际上是为了保证项目的质量和开发体验,因此原则上不会作到全量的覆盖。

由于目前咱们的项目大多属于敏捷开发,UI 样式的改动或者功能性需求较多,时间上也没法容许咱们作到更好的测试覆盖。

所以,咱们书写测试的目标是抽象出来的功能函数(集中放在 modules 文件夹),对数据流操做的 action,公共的组件(components 里 comon 文件夹下)。

只有单元测试和 UI 测试会计算到测试覆盖率,而 e2e 不会被计算进去。e2e 不须要写太多,由于大部分关键逻辑已经被单元测试覆盖,e2e 只须要简单的进行主流程的模拟。

经过 Jest 里的 collectCoverageFrom 配置改变测试统计的范围,最终项目的测试覆盖率要求为 Statement 60%, Branches 60%, Functions 60%, Lines 60%. 相关配置以下:

{
  collectCoverageFrom: [
    'src/components/common/**/*.{js,jsx,ts,tsx}',
    'src/modules/**/*.{js,jsx,ts,tsx}',
    'src/lib/**/*.{js,jsx,ts,tsx}',
    'src/store/**/*.{js,jsx,ts,tsx}',
    'src/constants/**/*.{js,jsx,ts,tsx}',
    'src/api/**/*.{js,jsx,ts,tsx}',
  ],
  coverageThreshold: {
    global: {
      statements: 60,
      branches: 60,
      functions: 60,
      lines: 60,
    },
  },
}
复制代码

这边展现下咱们项目里 store/actions 文件夹下的测试覆盖状况

能够在最上面看到整个文件夹的整体的测试覆盖状况,和下面每一个文件的具体覆盖状况。点击文件进去还能查看具体代码的覆盖状况。

总结

为项目添加测试是有必定成本的,尤为是 UI 测试方面。任何一件事情咱们都须要平衡成本和收益,就像上文提到的,成本低的单元测试尽量的全量覆盖,而高成本的 UI 测试则只作公共组件的覆盖。

前端测试确实会给项目带来至关多的好处,它能为 长期迭代 的项目带来显著的质量提高。

  • 首先是能在测试环境下降 bug 数量,经过运行单测能检测出一些逻辑错误。
  • 其次覆盖到很多 QA 同窗没有覆盖到的边界状况(笔者在后期补写测试的时候,顺手修了几个问题😁),由于咱们的测试编写原则就是要充分考虑数据的边界条件。
  • 可以方便重构。
  • 在原有逻辑增长新功能时,经过运行以前的测试,可以大大提升迭代的质量和稳定性。

这篇文章主要总结了笔者在 React 项目中书写测试的经验与沉淀,而对于 Vue 的项目,暂时尚未深刻研究。可是 Vue 有个特色,基本上重要的库好比 Vue-Router, Vuex 都是官方维护,一样的 Vue Test Utils 也是 Vue.js 官方的单元测试工具库。文档 写的至关详细,对 Vue 项目编写测试时能够参考。