jest + enzyme单元测试

安装

  • create-react-app创建的应用自带Jest库,执行命令npm run test就会进入测试单元界面。同时提供强行运行所有单元测试代码、选择只用心满足过滤条件的单元测试用例等高级功能。
  • 在项目中,我们配置了相关的参数,具体的配置如下

文件位置,以及目录写法

我们使用单元测试,一般的写法,是在待测试的方法或事组件的同级目录下,声明一个 __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', // 测试环境
  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环境, 并可以使用jQuery的方式访问组件的信息
  • render:静态渲染,它将React组件渲染成静态的HTML字符串,然后使用Cheerio这个库解析这段字符串,并返回一个Cheerio的实例对象,可以用来分析组件的html结构
  • mount:完全渲染,它将组件渲染加载成一个真实的DOM节点,用来测试DOM API的交互和组件的生命周期。用到了jsdom来模拟浏览器环境
常见方法
  • simulate(event, mock):模拟事件,用来触发事件,event为事件名称,mock为一个event object
  • instance():返回组件的实例
  • find(selector):根据选择器查找节点,selector可以是CSS中的选择器,或者是组件的构造函数,组件的display name
  • at(index):返回一个渲染过的对象
  • get(index):返回一个react node,要测试它,需要重新渲染
  • contains(nodeOrNodes):当前对象是否包含参数重点 node,参数类型为react对象或对象数组
  • text():返回当前组件的文本内容
  • html(): 返回当前组件的HTML代码形式
  • props():返回根组件的所有属性
  • prop(key):返回根组件的指定属性
  • state():返回根组件的状态
  • setState(nextState):设置根组件的状态
  • setProps(nextProps):设置根组件的属性

编写测试用例

it

每个测试用例一个it函数代表,当然it还有一个别名,叫做test

我们先看一个例子:

// ,第一个参数描述它的预期行为
it('当我们传入的参数都是数字的时候', () = > {
  // 增加断言语句
})
  • 第一个参数:用来表示我们预期这个测试用例的行为
  • 第二个参数:是我们实际进行测试的逻辑代码书写的位置
describe

这个是一个测试的套件,在这个里面我们可以写多个it函数,这个的主要作用就是,我们有的时候,可能也需要对测试用例进行分类,这个时候,我们就用到了describe。我们看一下他的具体的写法

describe('这是一个小的逻辑单元', () => {
  it('当我们传入的参数都是数字的时候', () => {
  })

 it('当我们传入的参数都是英文的时候', () => {
  })
})
  • beforeAll在开始测试套件之前执行一次(在beforeEach之前)
  • afterAll 在结束测试套件中所有测试用例之后执行一次在(afterEach之后)
  • beforeEach 每个测试用例在执行之前都执行一次
  • afterEach 每个测试用例在执行之后都执行一次

常见的断言

  • expect(value):要测试一个值进行断言的时候,要使用expect对值进行包裹
  • toBe(value):使用Object.is来进行比较,如果进行浮点数的比较,要使用toBeCloseTo
  • not:用来取反
  • toEqual(value):用于对象的深比较
  • toMatch(regexpOrString):用来检查字符串是否匹配,可以是正则表达式或者字符串
  • toContain(item):用来判断item是否在一个数组中,也可以用于字符串的判断
  • 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被调用的次数
  • assertions(number):验证在一个测试用例中有number个断言被调用
  • extend(matchers):自定义一些断言

实战

对于方法的测试用例的编写

//bytesCount: 获取字符串长度,英文占一个字符,中文占两个字符
import { bytesCount } from '../bytesCount';

describe('测试计算字符串长度', () => {
  it('当传入的字符串为汉字', () => {
    expect(bytesCount('哈哈哈哈')).toBe(8);
  });
  it('当传入的字符串为数字', () => {
    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('当传入的字符串为数组', () => {
    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的时候,找不到节点
  • 我们使用react框架,在通过节点进行取值的时候,由于是虚拟dom,会拿不到节点
  • 我们为了简化路径,在webpack中配置了公共的路径,但是在在单元测试中,引入在webpack中配置的路径,会有问题。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容