jest + enzyme單元測試

安裝

  • create-react-app創(chuàng)建的應用自帶Jest庫,執(zhí)行命令npm run test就會進入測試單元界面。同時提供強行運行所有單元測試代碼、選擇只用心滿足過濾條件的單元測試用例等高級功能。
  • 在項目中,我們配置了相關的參數(shù),具體的配置如下

文件位置,以及目錄寫法

我們使用單元測試,一般的寫法,是在待測試的方法或事組件的同級目錄下,聲明一個 __test__的文件夾,并在該文件夾下,命名一個以.test.js結尾的文件。

jest.config.js

module.exports = {
  collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts'],
  coverageDirectory: '<rootDir>/.tmp/coverage',
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: -10
    }
  },
  moduleNameMapper: {
    '^react-native$': 'react-native-web',
    '^.+\\.module\\.(css|sass|scss)$': 'identity-obj-proxy'
  }, // 代表需要被Mock的資源名稱
  moduleFileExtensions: [
    'web.js',
    'js',
    'web.ts',
    'ts',
    'web.tsx',
    'tsx',
    'json',
    'web.jsx',
    'jsx',
    'node'
  ], // 代表支持加載的文件名
  resolver: 'jest-pnp-resolver',
  setupFiles: ['react-app-polyfill/jsdom'],
  // 配置`setupTests.js`中`enzyme`的連接,使得`enzyme`配置生效
  setupTestFrameworkScriptFile: '<rootDir>/src/setupTests.js',
  testMatch: [
    '<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
    '<rootDir>/src/**/?(*.)(spec|test).{js,jsx,ts,tsx}'
  ], // 配置找到_test_下面的以Component.test.js,Component.spec.js
  testEnvironment: 'jsdom', // 測試環(huán)境
  testURL: 'http://localhost', // 它反映在諸如location.href之類的屬性中
  transform: {
    '^.+\\.(js|jsx|ts|tsx)$': '<rootDir>/node_modules/babel-jest',
    '^.+\\.css$': '<rootDir>/config/jest/cssTransform.js',
    '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)':
      '<rootDir>/config/jest/fileTransform.js'
  },
  transformIgnorePatterns: [
    '[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$',
    '^.+\\.module\\.(css|sass|scss)$'
  ],
  verbose: false
};

setUpTest.js

import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

configure({ adapter: new Adapter() });

package.json的啟動腳本配置

 "scripts": {
    "test": "node scripts/test.js",
  },

script下面test.js

'use strict';

// Do this as the first thing so that any code reading it knows the right env.
process.env.BABEL_ENV = 'test';
process.env.NODE_ENV = 'test';
process.env.PUBLIC_URL = '';

// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on('unhandledRejection', err => {
  throw err;
});

// Ensure environment variables are read.
require('../config/env');

const { execSync } = require('child_process');
const jest = require('jest');

const argv = process.argv.slice(2);

function isInGitRepository() {
  try {
    execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });

    return true;
  } catch (e) {
    return false;
  }
}

function isInMercurialRepository() {
  try {
    execSync('hg --cwd . root', { stdio: 'ignore' });

    return true;
  } catch (e) {
    return false;
  }
}

// Watch unless on CI, in coverage mode, or explicitly running all tests
if (!process.env.CI && argv.indexOf('--coverage') === -1 && argv.indexOf('--watchAll') === -1) {
  // https://github.com/facebook/create-react-app/issues/5210
  const hasSourceControl = isInGitRepository() || isInMercurialRepository();

  argv.push(hasSourceControl ? '--watch' : '--watchAll');
}

jest.run(argv);

enzyme
三種渲染方式
  • shallow:淺渲染,是對官方的Shallow Renderer的封裝。將組件渲染成虛擬DOM對象,只會渲染第一層,子組件將不會被渲染出來,使得效率非常高。不需要DOM環(huán)境, 并可以使用jQuery的方式訪問組件的信息
  • render:靜態(tài)渲染,它將React組件渲染成靜態(tài)的HTML字符串,然后使用Cheerio這個庫解析這段字符串,并返回一個Cheerio的實例對象,可以用來分析組件的html結構
  • mount:完全渲染,它將組件渲染加載成一個真實的DOM節(jié)點,用來測試DOM API的交互和組件的生命周期。用到了jsdom來模擬瀏覽器環(huán)境
常見方法
  • simulate(event, mock):模擬事件,用來觸發(fā)事件,event為事件名稱,mock為一個event object
  • instance():返回組件的實例
  • find(selector):根據(jù)選擇器查找節(jié)點,selector可以是CSS中的選擇器,或者是組件的構造函數(shù),組件的display name
  • at(index):返回一個渲染過的對象
  • get(index):返回一個react node,要測試它,需要重新渲染
  • contains(nodeOrNodes):當前對象是否包含參數(shù)重點 node,參數(shù)類型為react對象或對象數(shù)組
  • text():返回當前組件的文本內容
  • html(): 返回當前組件的HTML代碼形式
  • props():返回根組件的所有屬性
  • prop(key):返回根組件的指定屬性
  • state():返回根組件的狀態(tài)
  • setState(nextState):設置根組件的狀態(tài)
  • setProps(nextProps):設置根組件的屬性

編寫測試用例

it

每個測試用例一個it函數(shù)代表,當然it還有一個別名,叫做test。

我們先看一個例子:

// ,第一個參數(shù)描述它的預期行為
it('當我們傳入的參數(shù)都是數(shù)字的時候', () = > {
  // 增加斷言語句
})
  • 第一個參數(shù):用來表示我們預期這個測試用例的行為
  • 第二個參數(shù):是我們實際進行測試的邏輯代碼書寫的位置
describe

這個是一個測試的套件,在這個里面我們可以寫多個it函數(shù),這個的主要作用就是,我們有的時候,可能也需要對測試用例進行分類,這個時候,我們就用到了describe。我們看一下他的具體的寫法

describe('這是一個小的邏輯單元', () => {
  it('當我們傳入的參數(shù)都是數(shù)字的時候', () => {
  })

 it('當我們傳入的參數(shù)都是英文的時候', () => {
  })
})
  • beforeAll在開始測試套件之前執(zhí)行一次(在beforeEach之前)
  • afterAll 在結束測試套件中所有測試用例之后執(zhí)行一次在(afterEach之后)
  • beforeEach 每個測試用例在執(zhí)行之前都執(zhí)行一次
  • afterEach 每個測試用例在執(zhí)行之后都執(zhí)行一次

常見的斷言

  • expect(value):要測試一個值進行斷言的時候,要使用expect對值進行包裹
  • toBe(value):使用Object.is來進行比較,如果進行浮點數(shù)的比較,要使用toBeCloseTo
  • not:用來取反
  • toEqual(value):用于對象的深比較
  • toMatch(regexpOrString):用來檢查字符串是否匹配,可以是正則表達式或者字符串
  • toContain(item):用來判斷item是否在一個數(shù)組中,也可以用于字符串的判斷
  • toBeNull(value):只匹配null
  • toBeUndefined(value):只匹配undefined
  • toBeDefined(value):與toBeUndefined相反
  • toBeTruthy(value):匹配任何使if語句為真的值
  • toBeFalsy(value):匹配任何使if語句為假的值
  • toBeGreaterThan(number): 大于
  • toBeGreaterThanOrEqual(number):大于等于
  • toBeLessThan(number):小于
  • toBeLessThanOrEqual(number):小于等于
  • toBeInstanceOf(class):判斷是不是class的實例
  • anything(value):匹配除了nullundefined以外的所有值
  • resolves:用來取出promisefulfilled時包裹的值,支持鏈式調用
  • rejects:用來取出promiserejected時包裹的值,支持鏈式調用
  • toHaveBeenCalled():用來判斷mock function是否被調用過
  • toHaveBeenCalledTimes(number):用來判斷mock function被調用的次數(shù)
  • assertions(number):驗證在一個測試用例中有number個斷言被調用
  • extend(matchers):自定義一些斷言

實戰(zhàn)

對于方法的測試用例的編寫

//bytesCount: 獲取字符串長度,英文占一個字符,中文占兩個字符
import { bytesCount } from '../bytesCount';

describe('測試計算字符串長度', () => {
  it('當傳入的字符串為漢字', () => {
    expect(bytesCount('哈哈哈哈')).toBe(8);
  });
  it('當傳入的字符串為數(shù)字', () => {
    expect(bytesCount(123)).toBe(3);
  });
  it('當傳入的字符串為英文', () => {
    expect(bytesCount('abd')).toBe(3);
  });
  it('當傳入的字符串為特殊字符', () => {
    expect(bytesCount('~|@%&*')).toBe(6);
  });

  it('當傳入的字符串為null', () => {
    expect(bytesCount(null)).toBe(0);
  });
  it('當傳入的字符串為undefined', () => {
    expect(bytesCount(undefined)).toBe(0);
  });

  it('當傳入的字符串為數(shù)組', () => {
    expect(bytesCount([])).toBe(0);
  });
});

對于組件的測試用例的書寫


//ArInput: 組件的作用,是用來在輸入框中輸入,當檢測到,或是換行的時候,會在輸入框下生成一個標簽
import React from 'react';
import { mount, shallow } from 'enzyme';
import ArInput from '../index';

let wrapper;
const props = {
  max: 5,
  callback: jest.fn(),
  tagsChange: jest.fn(),
  placeholder: '非英文輸入',
  tagsInit: [123, 333]
};

describe('Test MonthPicker Component', () => {
  beforeEach(() => {
    wrapper = mount(<ArInput {...props} />);
  });

  it('初始化生成的標簽是否正常', () => {
    expect(wrapper.find('.ant-tag').length).toEqual(2);
  });

  it('placeHolder顯示是否正常', () => {
    expect(wrapper.find('input').getDOMNode().placeholder).toEqual('非英文輸入');
  });

  it('輸入事件是否可以正確的使用', () => {
    const mockEvent = {
      target: {
        value: '13,'
      }
    };
    wrapper.find('input').simulate('keyup', mockEvent);
    expect(props.callback).toHaveBeenCalledWith('13,');
    expect(wrapper.state('tags').length).toEqual(3);
    expect(wrapper.state('tags')[2]).toEqual('13');
  });
});

踩過的坑

  • 在項目中引入了antdesign,所有我們在進行配置的時候,渲染的方式只能選用mount,這樣的話,渲染的會有一點慢
  • 我們使用了css-module的形式來編寫樣式,這會導致我們在通過類名查找dom的時候,找不到節(jié)點
  • 我們使用react框架,在通過節(jié)點進行取值的時候,由于是虛擬dom,會拿不到節(jié)點
  • 我們?yōu)榱撕喕窂?,?code>webpack中配置了公共的路徑,但是在在單元測試中,引入在webpack中配置的路徑,會有問題。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容