點餐前端測試最佳實踐

做者介紹:柯培霖,美團點評工程師。前端

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 項目編寫測試時能夠參考。

相關文章
相關標籤/搜索