前端必备的测试
前端必备的测试
DellLee 老师的 前端要学的测试课 从Jest入门到TDD/BDD双实战学习记录。
知识点
Jest 基础
基础API
、 异步测试
、 Mock技巧
、 快照
、 timer测试
、 Dom测试
实现项目
Vue
、 Vue-test-utils
、 React
、 Enzyme
、 TDD+单元测试
、 BDD+集成测试
初识测试
// math.js
function add(a, b) {
return a + b;
}
function minus(a, b) {
return a - 'b'
}
export {
add,
minus
}
// math.test.js
// 简陋测试
import {
add,
minus
} from './math'
let result
let expected
result = add(3, 7)
expected = 10;
if (result !== 10) {
throw Error(`3 + 7 应该等于 ${expected},但结果却是 ${result}`)
}
result = minus(3, 3)
expected = 0;
if (result !== 0) {
throw Error(`3 - '3 应该等于 ${expected},但结果却是 ${result}`)
'
}
// 封装版
import {
add,
minus
} from './math'
function expect(result) {
return {
toBe: function(actual) {
if (result !== actual) {
throw new Error(`预期值与实际值不相等 预期${actual} 结果却是 ${result}`)
}
}
}
}
function test(desc, fn) {
try {
fn();
console.log(`${desc}通过测试`)
} catch (e) {
console.log(`${desc}没有通过测试 ${e}`)
}
}
test('测试加法3 + 7', () => {
expect(add(3, 7)).toBe(6)
})
test('测试减法6 - '
3 ', () => {'
expect(minus(6, 3)).toBe(6)
})
自动化框架:Jest
优点:性能、功能、易用性、速度快、Api简单、易配置、隔离性好、监控模式、IDE整合、Snapshot、多项目并行、覆盖率、Mock丰富
简单配置
# 项目内部调用 jest
npx jest --init
# 选择
browser-like
# 随后选择自动生成报告,自动清除实例在每个test之前,就会生成一下配置文件
jest.config.js
jest.config.js
// For a detailed explanation regarding each configuration property, visit:
// https://jestjs.io/docs/en/configuration.html
module.exports = {
// All imported modules in your tests should be mocked automatically
// 自动模拟在测试用例中的所有导入模块,在 __mocks__ 文件夹中寻找
// automock: false,
// Stop running tests after `n` failures
// 默认情况下,Jest运行所有测试并在完成后将所有错误生成到控制台,bil 让 jest 在 n 失败后停止运行测试
// bail: 0,
// Respect "browser" field in package.json when resolving modules
// false => browser | true => node
// browser: false,
// The directory where Jest should store its cached dependency information
// 存放 jest 依赖信息缓存的目录
// cacheDirectory: "C:\\Users\\Administrator\\AppData\\Local\\Temp\\jest",
// Automatically clear mock calls and instances between every test
// 自动清除模拟调用和实例在每次测试之间
clearMocks: true,
// Indicates whether the coverage information should be collected while executing the test
// 是否收集测试时的覆盖率信息,因为要带上覆盖率搜集语句访问所有执行过的文件,这可能会让测试执行速度明显减慢
// collectCoverage: false,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// 指示应收集覆盖率信息的全局模式一组文件,即使文件不存在测试,也将为其收集覆盖率信息,并且测试套件中不需要它
// collectCoverageFrom: null,
// The directory where Jest should output its coverage files
// jest 输出测速覆盖率文件的目录
// 运行 npx jest --coverage
coverageDirectory: 'coverage'
// An array of regexp pattern strings used to skip coverage collection
// 忽略测试的文件路径的正则匹配
// coveragePathIgnorePatterns: [
// "\\\\node_modules\\\\"
// ],
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: null,
// A path to a custom dependency extractor
// dependencyExtractor: null,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: null,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: null,
// A set of global variables that need to be available in all test environments
// globals: {},
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
// 模块使用文件扩展名数组,当你导入的文件没有扩展名的时候,它会在这个数组里面去自动匹配
// moduleFileExtensions: [
// "js",
// "json",
// "jsx",
// "ts",
// "tsx",
// "node"
// ],
// A map from regular expressions to module names that allow to stub out resources with a single module
// 模块名映射,类 webpack alias 以及 jsconfig.js 的 compilerOptions.paths
// moduleNameMapper: {},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// Activates notifications for test results
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
// preset: null,
// Run tests from one or more projects
// projects: null,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// Automatically reset mock state between every test
// resetMocks: false,
// Reset the module registry before running each individual test
// resetModules: false,
// A path to a custom resolver
// resolver: null,
// Automatically restore mock state between every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// rootDir: null,
// A list of paths to directories that Jest should use to search for files in
// roots: [
// "<rootDir>"
// ],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// 运行做测试的时候使用某些垫片为运行环境做兼容
// setupFilesAfterEnv: [],
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// 快照格式化
// snapshotSerializers: [],
// The test environment that will be used for testing
// testEnvironment: "jest-environment-jsdom",
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
// 匹配测试文件
// testMatch: [
// "**/__tests__/**/*.[jt]s?(x)",
// "**/?(*.)+(spec|test).[tj]s?(x)"
// ],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "\\\\node_modules\\\\"
// ],
// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
// This option allows the use of a custom results processor
// testResultsProcessor: null,
// This option allows use of a custom test runner
// testRunner: "jasmine2",
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
// 模拟浏览器的地址
// testURL: "http://localhost",
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
// timers: "real",
// A map from regular expressions to paths to transformers
// 不同文件类型对应不同的转换器
// transform: null,
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "\\\\node_modules\\\\"
// ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
// verbose: null,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// watchman: true,
};
babelrc
jest 未配置转换时,默认只支持 commonjs 语法。
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
]
]
}
jest转换(commonjs -> es module)大概工作原理:
- npm run jest
- jest(babel-jest)jest 内部的
- 检测是有 babel-core
- 拿到 .babelrc 配置
- 再运行测试之前,结合 babel 把代码做一次转化
- 运行转化过的测试用例
常用匹配器(Matchers)
数字
.toBe()
.toEqual(value)
.toBeGreaterThan(number)
.toBeGreaterThanOrEqual(number)
.toBeLessThan(number)
.toBeLessThanOrEqual(number)
.toBeCloseTo(number, numDigits?)
第二个参数为精度,代表几位小数点,默认为2位
.toBeNaN()
// 数字相关
test('匹配器:toBe-数字相等', () => {
// toBe 匹配器 matchers Object.is() ===
const a = 10;
expect(a).toBe(10);
});
test('匹配器:toEqual-内容相等', () => {
// toEqual 匹配器 matchers
const a = {
one: 1
};
expect(a).toEqual({
one: 1
});
});
test('匹配器:toBeGreaterThan-大于比较数', () => {
// toBeGreaterThan 匹配器 matchers
const a = 4;
expect(a).toBeGreaterThan(3);
});
test('匹配器:toBeGreaterThanOrEqual-大于等于比较数', () => {
// toBeGreaterThanOrEqual 匹配器 matchers
const a = 4;
expect(a).toBeGreaterThanOrEqual(4);
});
test('匹配器:toBeLessThan-小于比较数', () => {
// toBeLessThan 匹配器 matchers
const a = 4;
expect(a).toBeLessThan(5);
});
test('匹配器:toBeLessThanOrEqual-小于等于比较数', () => {
// toBeLessThanOrEqual 匹配器 matchers
const a = 4;
expect(a).toBeLessThanOrEqual(4);
});
// 浮点数
test('匹配器:toBeCloseTo-两个浮点数字相加', () => {
// toBeCloseTo 匹配器 matchers
const value = 0.1 + 0.2;
// expect(value).toBe(0.3); 这句会报错,因为浮点数有舍入误差
expect(value).toBeCloseTo(0.3); // 这句可以运行
});
// NaN
test('匹配器:toBeNaN-等于NaN', () => {
expect(NaN).toBeNaN();
expect(1).not.toBeNaN();
});
真假
- '
toBeNull
只匹配null
' - '
toBeUndefined
只匹配undefined
' - '
toBeDefined
与toBeUndefined
相反' - '
toBeTruthy
匹配任何if
语句为真' - '
toBeFalsy
匹配任何if
语句为假'
.toBeNull()
.toBeUndefined()
.toBeDefined()
.toBeTruthy()
.toBeFalsy()
在JavaScript中,有六个falsy值: false
, 0
, ''
, null
, undefined
,和 NaN
。其他一切都是真实的。
// 真假相关
test('匹配器:toBeNull-与null相等', () => {
// toBeNull 匹配器 matchers
const a = null;
expect(a).toBeNull();
});
test('匹配器:toBeUndefined-与undefined相等', () => {
// toBeUndefined 匹配器 matchers
const a = undefined;
expect(a).toBeUndefined();
});
test('匹配器:toBeDefined-被定义过的,非 undefined 的,可为 null', () => {
// toBeDefined 匹配器 matchers
const a = null;
expect(a).toBeDefined();
});
test('匹配器:toBeTruthy-真值或者隐藏为true的', () => {
// toBeTruthy 匹配器 matchers
const a = 1;
expect(a).toBeTruthy();
});
test('匹配器:toBeFalsy-真值或者隐藏为false的', () => {
// toBeFalsy 匹配器 matchers
const a = null;
expect(a).toBeFalsy();
});
否定
.not
test('匹配器:not-不是xx', () => {
// not 匹配器 matchers
const a = 1;
expect(a).not.toBeFalsy();
});
expect.not.arrayContaining(array)
匹配不是接收值的子集
describe('not.arrayContaining', () => {
const expected = [1];
it('如果接收的数组不包含1就通过测试', () => {
expect([2, 3, 4]).toEqual(expect.not.arrayContaining(expected));
});
});
expect.not.objectContaining(object)
匹配不包含某个对象
describe('not.objectContaining', () => {
const expected = {
haha: 'laibh.top'
};
it('如果接收的对象不包含{ haha: laibh.top }就通过测试', () => {
expect({
haha: 'laibh.top1'
}).toEqual(expect.not.objectContaining(expected));
});
});
expect.not.stringContaining(string)
匹配不包含某个字符串
describe('not.stringContaining', () => {
const expected = '赖同学';
it('如果接收的字符串不完全等于赖同学就通过测试', () => {
expect('赖').toEqual(expect.not.stringContaining(expected));
});
});
expect.not.stringMatching(string|regexp)
同上,不过参数可以是正则表达式
describe('not.stringMatching', () => {
const expected = /赖同学/;
it('如果接收的字符串赖同学就通过测试', () => {
expect('赖').toEqual(expect.not.stringMatching(expected));
});
});
字符串
.toMatch( regexpOrString )
// 字符串
test('匹配器:toMatch-正则字符串匹配', () => {
// toMatch 匹配器 matchers
const str = 'http://laibh.top';
expect(str).toMatch('laibh');
// 使用正则
expect(str).toMatch(/laibh/);
});
expect.stringContaining(string)
匹配包含某个字符串
expect.stringMatching(string|regexp)
匹配字符串,可用正则
describe('stringMatching in arrayContaining', () => {
const expected = [expect.stringMatching(/^Alic/), expect.stringMatching(/^[BR]ob/)];
it('matches even if received contains additional elements', () => {
expect(['Alicia', 'Roberto', 'Evelina']).toEqual(expect.arrayContaining(expected));
});
it('does not match if received does not contain expected elements', () => {
expect(['Roberto', 'Evelina']).not.toEqual(expect.arrayContaining(expected));
});
});
数组
expect.toContain()
// 数组
test('匹配器:toContain-数组包含某项', () => {
// toContain 匹配器 matchers
const arr = ['lai', 'bin', 'hong'];
const set = new Set(arr);
expect(set).toContain('lai');
expect(arr).toContain('lai');
});
expect.arrayContaining(array)
匹配子集
describe('arrayContaining', () => {
const expected = ['1', '2'];
it('即使接收值包含其他参数也匹配', () => {
expect(['1', '2', '3']).toEqual(expect.arrayContaining(expected));
});
it('只要接收值不包含期望的值就不匹配', () => {
expect(['2', '4']).not.toEqual(expect.arrayContaining(expected));
});
});
对象
objectContaining(object)
匹配任何递归预期属性的接收对象
test('测试onPress函数回调参数匹配对象', () => {
const onPress = jest.fn();
simulatePresses(onPress);
expect(onPress).toBeCalledWith(
expect.objectContaining({
x: expect.any(Number),
y: expect.any(Number)
})
);
});
.toHaveProperty(keyPath, value?)
检查对象中各种属性存在和值,第二个参数是可选的
// Object containing house features to be tested
const houseForSale = {
bath: true,
bedrooms: 4,
kitchen: {
amenities: ['oven', 'stove', 'washer'],
area: 20,
wallColor: 'white',
'nice.oven': true
},
'ceiling.height': 2
};
test('this house has my desired features', () => {
// Simple Referencing
expect(houseForSale).toHaveProperty('bath');
expect(houseForSale).toHaveProperty('bedrooms', 4);
expect(houseForSale).not.toHaveProperty('pool');
// 嵌套深层用 .
expect(houseForSale).toHaveProperty('kitchen.area', 20);
expect(houseForSale).toHaveProperty('kitchen.amenities', ['oven', 'stove', 'washer']);
expect(houseForSale).not.toHaveProperty('kitchen.open');
// 嵌套深层用 []
expect(houseForSale).toHaveProperty(['kitchen', 'area'], 20);
expect(houseForSale).toHaveProperty(['kitchen', 'amenities'], ['oven', 'stove', 'washer']);
expect(houseForSale).toHaveProperty(['kitchen', 'amenities', 0], 'oven');
expect(houseForSale).toHaveProperty(['kitchen', 'nice.oven']);
expect(houseForSale).not.toHaveProperty(['kitchen', 'open']);
// Referencing keys with dot in the key itself
expect(houseForSale).toHaveProperty(['ceiling.height'], 'tall');
});
.toMatchObject(object)
检查对象的属性的子集相匹配
const houseForSale = {
bath: true,
bedrooms: 4,
kitchen: {
amenities: ['oven', 'stove', 'washer'],
area: 20,
wallColor: 'white'
}
};
const desiredHouse = {
bath: true,
kitchen: {
amenities: ['oven', 'stove', 'washer'],
wallColor: expect.stringMatching(/white|yellow/)
}
};
test('the house has my desired features', () => {
expect(houseForSale).toMatchObject(desiredHouse);
});
类
.toBeInstanceOf(Class)
检查对象是一个类的实例
class A {}
expect(new A()).toBeInstanceOf(A);
expect(() => {}).toBeInstanceOf(Function);
expect(new A()).toBeInstanceOf(Function); // throws
异常
.toThrow(error?)
const throwNewErrorFunc = () => {
throw new Error('this is a new error');
};
// 异常
test('匹配器:toThrow-测试抛出异常', () => {
// toThrow 匹配器 matchers
expect(throwNewErrorFunc).toThrow();
// 测试抛出的内容
expect(throwNewErrorFunc).toThrow('this is a new error');
// 表达式也行
expect(throwNewErrorFunc).toThrow(/this is a new error/);
});
任意
expect.anything()
匹配除了 null
、 undefined
的任意值,可以它使用在 toEqual
或者 toBeCalledWith
里面替代文字值
test('map 遍历一个非空的参数', () => {
const mock = jest.fn();
[1].map(x => mock(x));
expect(mock).toBeCalledWith(expect.anything());
});
expect.any(constructor)
匹配任意构造器生成的实例
function randocall(fn) {
return fn(Math.floor(Math.random() * 6 + 1));
}
test('测试 randocall的回调函数调用了一个数字', () => {
const mock = jest.fn();
randocall(mock);
expect(mock).toBeCalledWith(expect.any(Number));
});
.toHaveLength(number)
检查对象有个 length 属性并将设为某一数值。这对于检查数组或字符串大小特别有用。
expect([1, 2, 3]).toHaveLength(3);
expect('abc').toHaveLength(3);
expect('').not.toHaveLength(5);
.toContain(item)
检查项目在数组或者字符串是否是另一个字符串的子串
test('the flavor list contains lime', () => {
expect(getAllFlavors()).toContain('lime');
});
.toContainEqual(item)
检查具有特定结构和值的元素是否包含在数据中
it('test contain', () => {
const testValue = {
bol: true
};
expect([
{
bol: true
},
{
sour: false
}
]).toContainEqual(testValue);
});
异步相关
expect.assertions()
匹配在测试用例里面使用断言的次数。确保在异步函数为了确保在回调函数里面断言被调用特别有用。
test('异步调用所有回调', () => {
// 确保有两个断言被调用
expect.assertions(2);
function callback1(data) {
expect(data).toBeTruthy();
}
function callback2(data) {
expect(data).toBeTruthy();
}
doAsync(callback1, callback2);
});
expect.hasAssertions()
匹配在测试用例里面至少使用一次断言
.resolves
使用 resolves
解开 fulfilled promise,如果 reject promise,则断言失败
test('resolves to lemon', () => {
// make sure to add a return statement
return expect(Promise.resolve('lemon')).resolves.toBe('lemon');
});
测试是异步的,需要告诉 jest 等待返回解开的断言
也可以用 async/await
结合 .resolves
test('resolves to lemon', async () => {
await expect(Promise.resolve('lemon')).resolves.toBe('lemon');
await expect(Promise.resolve('lemon')).resolves.not.toBe('octopus');
});
.rejects
使用 rejects
解开 rejected promise, 如果 fulfilled promise, 则断言失败
test('rejects to octopus', async () => {
// make sure to add a return statement
return expect(Promise.reject(new Error('octopus'))).rejects.toThrow('octopus');
});
测试是异步的,需要告诉 jest 等待返回解开的断言
同样使用 async/await
结合 .rejects
test('rejects to octopus', async () => {
await expect(Promise.reject(new Error('octopus'))).rejects.toThrow('octopus');
await expect(Promise.reject(new Error('octopus'))).rejects.not.toThrow('lemon');
});
函数调用
.toHaveBeenCalled()|.toBeCalled()
确保模拟功能得到调用
function drinkAll(cb, flavour) {
if (flavour !== 'octopus') {
cb(flavour);
}
}
describe('test drinkAll function', () => {
test('drink somthing lemon-flavour', () => {
const drink = jest.fn();
drinkAll(drink, 'lemon');
expect(drink).toBeCalled();
});
});
describe('test drinkAll function', () => {
test('does not drink somthing octopus-flavour', () => {
const drink = jest.fn();
drinkAll(drink, 'octopus');
expect(drink).not.toBeCalled();
});
});
describe('test drinkAll function', () => {
test('drink somthing lemon-flavour', () => {
const drink = jest.fn();
drinkAll(drink, 'lemon');
expect(drink).toHaveBeenCalled();
});
});
describe('test drinkAll function', () => {
test('does not drink somthing octopus-flavour', () => {
const drink = jest.fn();
drinkAll(drink, 'octopus');
expect(drink).not.toHaveBeenCalled();
});
});
.toHaveBeenCallTimes(number)|.toBeCalledTimes(number)
确保模拟功能得到调用次数与指定数字一致
test('drinkEach drinks each drink', () => {
const drink = jest.fn();
drinkEach(drink, ['lemon', 'octopus']);
expect(drink).toHaveBeenCalledTimes(2);
expect(drink).toBeCalledTimes(2);
});
.toHaveBeenCalledWith(arg1, arg2, ...)|.toBeCalledWith(arg1, arg2, ...)
确保模拟功能被调用的具体参数
function calledWithArg(cb) {
cb('Arg');
}
test('test calledWithArg', () => {
const fn = jest.fn();
calledWithArg(fn);
expect(fn).toBeCalledWith('Arg');
expect(fn).toHaveBeenCalledWith('Arg');
});
.toHaveBeenLastCalledWith(arg1, arg2, ...)|.lastCalledWith(arg1, arg2, ...)
确保模拟功能被最后一次调用的具体参数
.toHaveBeenNthCalledWith(nthCall, arg1, arg2, ....)|.nthCalledWith(nthCall, arg1, arg2, ....)
确保模拟功能多次调用的顺序
test('drinkEach drinks each drink', () => {
const drink = jest.fn();
drinkEach(drink, ['lemon', 'octopus']);
expect(drink).toHaveBeenNthCalledWith(1, 'lemon');
expect(drink).toHaveBeenNthCalledWith(2, 'octopus');
expect(drink).nthCalledWith(1, 'lemon');
expect(drink).nthCalledWith(2, 'octopus');
});
n 必须是从 1开始的正整数
.toHaveReturned()|.toReturn()
测试模拟函数成功返回(即没有抛出错误)至少一次
test('test calledWithArg', () => {
const fn = jest.fn();
calledWithArg(fn);
expect(fn).toHaveReturned();
expect(fn).toReturn();
});
.toHaveReturnedTimes(number)|.toReturnTimes(number)
确保模拟函数返回成功的次数, 抛出错误的模拟函数的任何调用都不计入函数返回的次数
test('drink returns twice', () => {
const drink = jest.fn(() => true);
drink();
drink();
expect(drink).toHaveReturnedTimes(2);
});
.toHaveReturnedWith(value)|.toReturnWith(value)
确保模拟函数返回特定的值
test('test Return 123', () => {
const fn = jest.fn(() => 123);
calledWithArg(fn);
expect(fn).toHaveReturnedWith(123);
expect(fn).toReturnWith(123);
});
.toHaveLastReturnedWith(value)|.lastReturnedWith(value)
确保模拟函数最后一次返回特定的值
.toHaveNthReturnedWith(nthCall, value)|.nthReturnedWith(nthCall, value)
确保模拟函数第n次调用返回特定的值
第n个参数必须是从1开始的正整数。
自定义扩展
expect.extend()
pass
表示是否有匹配, message
提供一个没有参数的函数,在出现错误的情况下返回消息。当 pass:false
, message
返回 expect(x).matcher()
失败的错误信息, pass:true
, message
返回当 expect(x).not.matcher()
失败时的错误消息
// expect.extend(matchers)
expect.extend({
toBeWithinRange(reveived, floor, ceiling) {
const pass = reveived >= floor && reveived <= ceiling;
if (pass) {
return {
message: () => `期待 ${reveived} 不在范围${floor}-${ceiling}内`,
pass
};
} else {
return {
message: () => `期待 ${reveived} 在范围${floor}-${ceiling}内`,
pass
};
}
}
});
test('测试范围', () => {
expect(100).toBeWithinRange(90, 100);
expect(101).not.toBeWithinRange(0, 100);
expect({
apples: 6,
bananas: 3
}).toEqual({
apples: expect.toBeWithinRange(1, 10),
bananas: expect.not.toBeWithinRange(11, 20)
});
});
异步扩展, 需要结合 async
和 await
函数来使用
expect.extend({
async toBeDivisibleByExternalValue(reveived) {
// 异步获取的除数
const externalValue = await getExternalValueFromRemoteSource();
const pass = received % externalValue == 0;
if (pass) {
return {
message: () => `期待${received}不被${externalValue}整除`,
pass
};
} else {
return {
message: () => `期待${received}被${externalValue}整除`,
pass
};
}
}
});
测试异步代码
回调形式
// fetchData.js
import request from '@/utils/request';
const fetchData = cb => {
request('http://www.dell-lee.com/react/api/demo.json').then(res => {
if (res.data) cb(res.data);
});
};
export default fetchData;
// fetchData.test.js
import fetchData from './fetchData';
// 错误
test('fetch 返回结果为 {success: true}', () => {
fetchData(data => {
expect(data).toEqual({
success: true
});
});
});
// 回调类型异步函数
test('fetch 返回结果为 {success: true}', done => {
fetchData(data => {
expect(data).toEqual({
success: true
});
done();
});
});
Promise
// fetchData.js
import request from '@/utils/request';
const fetchData = () => request('http://www.dell-lee.com/react/api/demo.json');
export default fetchData;
//fetchData.test.js
import fetchData from './fetchData';
// Promise
test('fetch 返回结果为 {success: true}', () => {
return fetchData().then(res => {
expect(res.data).toEqual({
success: true
});
});
});
// 测试404
test('fetchData 返回结果为404', () => {
// 要求至少跑一次 expect
expect.assertions(1);
return fetchData().catch(e => {
expect(e.toString().indexOf('404') > -1).toBe(true);
});
});
// .resolves / .rejects
test('fetch 返回结果为 {success: true}', () => {
return expect(fetchData()).resolves.toMatchObject({
data: {
success: true
}
});
});
test('fetchData 返回结果为404', () => {
return expect(fetchData()).rejects.toThrow();
});
// Async/Await
test('fetch 返回结果为 {success: true}', async () => {
await expect(fetchData()).resolves.toMatchObject({
data: {
success: true
}
});
});
test('fetchData 返回结果为404', async () => {
await expect(fetchData()).rejects.toThrow();
});
// 另一种 Async/Await
test('fetch 返回结果为 {success: true}', async () => {
const res = await fetchData();
expect(res.data).toEqual({
success: true
});
});
test('fetchData 返回结果为404', async () => {
expect.assertions(1);
try {
await fetchData();
} catch (e) {
await expect(e.toString()).toEqual('Error: Request failed with status code 404');
}
});
钩子函数
// Couter.js
class Counter {
constructor() {
this.number = 0;
}
addOne() {
this.number += 1;
}
addTwo() {
this.number += 2;
}
minusOne() {
this.number -= 1;
}
minusTwo() {
this.number -= 2;
}
}
export default Counter;
// Couter.test.js
import Counter from './Couter';
describe('测试 Counter', () => {
let couter;
// 所有测试开始之前
beforeAll(() => {
couter = new Counter();
});
// 每个测试用例开始之前
beforeEach(() => {
// 每次测试都会生成一个新的 couter
couter = new Counter();
});
// 每个测试用例结束之后
afterEach(() => {});
// 所有测试结束之后
afterAll(() => {});
describe('测试增加相关代码', () => {
test('测试 Conter 中的 addOne 方法', () => {
couter.addOne();
expect(couter.number).toBe(1);
});
test('测试 Conter 中的 addTwo 方法', () => {
couter.addTwo();
expect(couter.number).toBe(2);
});
});
describe('测试减少相关代码', () => {
test('测试 Conter 中的 minusOne 方法', () => {
couter.minusOne();
expect(couter.number).toBe(-1);
});
test('测试 Conter 中的 minusTwo 方法', () => {
couter.minusTwo();
expect(couter.number).toBe(-2);
});
});
});
describe 里面就是一个作用域,嵌套作用域可以有多个钩子函数,钩子函数执行顺序由外到内
test.only
,只执行某个测试用例
describe('测试减少相关代码', () => {
test.only('测试 Conter 中的 minusOne 方法', () => {
couter.minusOne();
expect(couter.number).toBe(-1);
});
test('测试 Conter 中的 minusTwo 方法', () => {
couter.minusTwo();
expect(couter.number).toBe(-2);
});
});
Mock
- 捕获函数的调用,this 指向,调用顺序
- 自由设置返回结果
- 改变内部函数的实现
基本模拟-导入函数
// demo
const runCallback = cb => {
cb();
};
export default runCallback;
//demo.test.js
import runCallback from './demo';
describe('测试 runCallback 方法', () => {
test('runCallback被调用,并只调用了一次', () => {
// mock 函数,捕获函数的调用
const func = jest.fn();
// 方法返回一次 值 Haha,mockReturnValue则是都返回,也可以在 jest.fn(()=>{return 'Haha'})定义
func.mockReturnValueOnce('Haha');
runCallback(func);
expect(func).toBeCalled();
expect(func.mock.calls.length).toBe(1);
expect(func.mock.results[0].value).toBe('Haha');
});
});
console.log(func.mock);
/**
● Console
console.log src/lesson2/demo.test.js:14
{ calls: [ [] ],
instances: [ undefined ],
invocationCallOrder: [ 1 ],
results: [ { type: 'return', value: 'Haha' } ] }
*/
修改原来函数的返回
// xx.js
import axios from 'axios';
const fetchData = () => {
axios.get('/').then(res => res.data);
};
// (function(){return '123' })()
export default fetchData;
// xx.test.js
import fetchData from './xx.js';
import axios from 'axios';
ject.mock('axios');
test('fetchData 测试', () => {
axios.get.mockResolvedValue({
data: "(function(){return '123' })()"
});
return fetchData().then(data => {
expect(eval(data)).toEqual('123');
});
});
__mocks__ 文件夹
// __mock__/xx.js
const fetchData = () => {
return new Promise((resolved, reject) => {
resolve("(function(){return '123' })()");
});
};
接着改测试,模拟函数
// xx.test.js
jest.mock('./xx');
import fetchData from './xx.js';
test('fetchData 测试', () => {
axios.get.mockResolvedValue({
data: "(function(){return '123' })()"
});
return fetchData().then(data => {
expect(eval(data)).toEqual('123');
});
});
上述函数顶部那步模拟也可以通过在 jest.config.js 中修改配置 automock:true
改为自动模拟,那么引入对应的函数的时候就会自动去 __mocks__
文件夹里面去寻找对应的模拟函数。注意一旦开启这个配置,需要启动才会生效,另外也会导致很多测试需要重新修改。
同时存在 __mocks__ 以及导入函数
在原来 xx.js 基础上面新增 一个 getNumber
函数
xx.js;
import axios from 'axios';
const fetchData = () => {
axios.get('/').then(res => res.data);
};
// (function(){return '123' })()
const getNumber = () => 123;
export { fetchData, getNumber };
接着修改测试用例
// xx.test.js
jest.mock('./xx');
import fetchData from './xx.js';
test('fetchData 测试', () => {
axios.get.mockResolvedValue({
data: "(function(){return '123' })()"
});
return fetchData().then(data => {
expect(eval(data)).toEqual('123');
});
});
test('getNumber 测试', () => {
expect(getNumber()).toBe(123);
});
上述代码会报错, getNumebr
找不到对应的函数,因为测试用例还是会去 __mocks__
文件夹去寻找 getNumber
。但是我们只希望模拟异步的函数,对于同步函数希望通过导入的方式来测试,那么就从原来的js文件中导入 getNumber
// xx.test.js
jest.mock('./xx');
import fetchData from './xx.js';
const { getNumber } = jest.requireActual('./xx');
test('fetchData 测试', () => {
axios.get.mockResolvedValue({
data: "(function(){return '123' })()"
});
return fetchData().then(data => {
expect(eval(data)).toEqual('123');
});
});
test('getNumber 测试', () => {
expect(getNumber()).toBe(123);
});
Mock Timer
写一个 setTimeout 函数
// timer.js
export default cb => {
setTimeout(() => {
cb();
}, 3000);
};
测试用例
// timer.test.js
import timer from './timer';
test('timer 测试', done => {
timer(() => {
expect(2).toBe(1);
done();
});
});
上面的测试用例会直接通过, timer
是一个异步函数,并不会执行函数体内的内容,需要像之前的异步函数一样,加个 done 参数
import timer from './timer';
test('timer 测试', done => {
timer(() => {
expect(2).toBe(1);
done();
});
});
接着测试用例便会运行,并报错
FAIL src/lesson3/timer.test.js (7.997s)
● Console
console.error node_modules/_jsdom@11.12.0@jsdom/lib/jsdom/virtual-console.js:29
Error: Uncaught [Error: expect(received).toBe(expected) // Object.is equality
Expected: 1
Received: 2]
at reportException (F:\赖彬鸿\git-project\usual\Egret-Project\FontendTest\node_modules\_jsdom@11.12.0@jsdom\lib\jsdom\living\helpers\runtime-script-errors.js:66:24)
at Timeout.callback [as _onTimeout] (F:\赖彬鸿\git-project\usual\Egret-Project\FontendTest\node_modules\_jsdom@11.12.0@jsdom\lib\jsdom\browser\Window.js:680:7)
at ontimeout (timers.js:436:11)
at tryOnTimeout (timers.js:300:5)
at listOnTimeout (timers.js:263:5)
at Timer.processTimers (timers.js:223:10) { Error: expect(received).toBe(expected) // Object.is equality
Expected: 1
Received: 2
at toBe (F:\赖彬鸿\git-project\usual\Egret-Project\FontendTest\src\lesson3\timer.test.js:5:15)
at cb (F:\赖彬鸿\git-project\usual\Egret-Project\FontendTest\src\lesson3\timer.js:3:5)
at Timeout.callback [as _onTimeout] (F:\赖彬鸿\git-project\usual\Egret-Project\FontendTest\node_modules\_jsdom@11.12.0@jsdom\lib\jsdom\browser\Window.js:678:19)
at ontimeout (timers.js:436:11)
at tryOnTimeout (timers.js:300:5)
at listOnTimeout (timers.js:263:5)
at Timer.processTimers (timers.js:223:10)
matcherResult:
{ actual: 2,
expected: 1,
message: [Function],
name: 'toBe',
pass: false } }
也可以通过模拟 timer 这类异步函数,来达到目的
// timer.test.js
import timer from './timer';
// mock timer
jest.useFakeTimers();
test('timer 测试', () => {
const fn = jest.fn();
timer(fn);
// 快速运行所有Timer
jest.runAllTimers();
expect(fn).toHaveBeenCalledTimes(1);
});
对于嵌套 timer 异步函数, jest.runOnlyPendingTimers
可以让只最外层的第一个 timer 运行。
另外还有快进时间的api, jest.advanceTimersByTime(n)
// timer.test.js
import timer from './timer';
jest.useFakeTimers();
test('timer 测试', () => {
const fn = jest.fn();
timer(fn);
jest.advanceTimersByTime(3000);
expect(fn).toHaveBeenCalledTimes(1);
});
嵌套 timer 的测试用例结合 钩子 beforeEach
以及两个上面讲的api的例子
// timer.js
export default cb => {
setTimeout(() => {
cb();
setTimeout(() => {
cb();
}, 3000);
}, 3000);
};
// timer.test.js
import timer from './timer';
beforeEach(() => {
jest.useFakeTimers();
});
test('timer 测试-runAllTimers', () => {
const fn = jest.fn();
timer(fn);
jest.runAllTimers();
expect(fn).toHaveBeenCalledTimes(2);
});
test('timer 测试-advanceTimersByTime', () => {
const fn = jest.fn();
timer(fn);
jest.advanceTimersByTime(3000);
expect(fn).toHaveBeenCalledTimes(1);
jest.advanceTimersByTime(3000);
expect(fn).toHaveBeenCalledTimes(2);
});
Mock Funtions
Mock 函数可以轻松测试代码之间的连接——实现方式包括:擦除函数实际实现、捕获对函数的调用(以及在这些调用中传递的参数)、在使用 new
实例化时捕获构造函数的实例,允许测试时配置返回值
模拟函数
测试函数 forEach
的内部实现,这个函数为传入的数组中的每个元素调用一次回调函数
function forEach(items, callback) {
for (let index = 0; index < items.length; index += 1) {
callback(item[index]);
}
}
为了测试此函数,可以使用一个 mock 函数,然后检查 mock 函数的状态来确保回调函数如期调用
const mockCallback = jest.fn(x => 42 + x);
forEach([0, 1], mockCallback);
// 此mock函数被调用了两次
expect(mockCallback.mock.calls.length).toBe(2);
console.log(mockCallback);
/**
{
[Function: mockConstructor]
_isMockFunction: true,
getMockImplementation: [Function],
mock: [Getter/Setter],
mockClear: [Function],
mockReset: [Function],
mockRestore: [Function],
mockReturnValueOnce: [Function],
mockResolvedValueOnce: [Function],
mockRejectedValueOnce: [Function],
mockReturnValue: [Function],
mockResolvedValue: [Function],
mockRejectedValue: [Function],
mockImplementationOnce: [Function],
mockImplementation: [Function],
mockReturnThis: [Function],
mockName: [Function],
getMockName: [Function]
}
*/
// 第一次调用函数时的第一个参数是 0
expect(mockCallback.mock.calls[0][0]).toBe(0);
console.log(mockCallback.mock);
/**
{
calls: [ [ 0 ], [ 1 ] ],
instances: [ undefined, undefined ],
invocationCallOrder: [ 1, 2 ],
results:
[ { type: 'return', value: 42 }, { type: 'return', value: 43 } ]
}
*/
// 第二次调用函数时的第一个参数是1
expect(mockCallback.mock.calls[1][0]).toBe(1);
// 第一次函数调用的返回值是 42
expect(mockCallback.mock.results[0].value).toBe(42);
.mock 属性
所有 mock 函数都有这个特殊的 .mock
属性,它保存了关于此函数如何调用、调用时的返回值的信息。 .mock
属性还追踪每次调用时 this
的值,所以我们同样也可以检视(inspect) this
const myMock = jest.fn();
const a = new myMock();
const b = {};
const bound = myMock.bind(b);
bound();
console.log(myMock.mock.instances);
// [ mockConstructor {}, {} ]
模拟返回值
const myMock = jest.fn();
console.log(myMock());
// undefined
myMock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);
console.log(myMock(), myMock(), myMock(), myMock());
// 10, 'x', true, true
结合一些函数灵活模拟
const filterTestFn = jest.fn();
filterTestFn.mockReturnValueOnce(true).mockReturnValueOnce(false);
const result = [11, 12].filter(filterTestFn);
console.log(result);
// [11]
console.log(filterTestFn.mock.calls);
// [[11],[12]]
模拟模块
// users.js
import axios from 'axios';
class Users {
static call() {
return axios.get('/user.json').then(res => res.data);
}
}
export default Users;
模拟 axios.get 返回一个假的 response
// users.test.js
import axios from 'axios';
import Users from './users';
jest.mock(axios);
test('should fetch users', () => {
const users = [
{
name: 'Bob'
}
];
const res = {
data: users
};
axios.get.mockResolvedValue(res);
return Users.all().then(data => expect(data).toEqual(users));
});
模拟实现
通过模拟函数 jest.fn
或者 mockImplementationOnce
方法来完成
const myMockFn = jest.fn(cb => cb(null, true));
myMockFn((err, val) => console.log(val));
// true
mockImplementation
当您需要定义从另一个模块创建的模拟函数的默认实现时,该方法很有用
// foo.js
module.exports = function () {
// some implementation
};
// test.js
jest.mock('../foo'); // 这个会自动模拟
const foo = require('../foo');
foo.mockImplementation(() => 42);
foo();
// 42
当需要重新创建模拟函数的复杂行为,以便多个函数调用产生不同的结果时,可以使用 mockImplementationOnce
方法
const myMockFn = jest
.fn()
.mockImplementationOnce(cb => cb(null, true))
.mockImplementationOnce(cb => cb(null, fase));
myMockFn((err, val) => console.log(val));
// true
myMockFn((err, val) => console.log(val));
// false
.mockReturnThis()
返回 this
const myObj = {
myMethod: jest.fn().mockReturnThis()
};
// 与下面实现相同
const otherObj = {
myMethod: jest.fn(function () {
return this;
})
};
模拟函数名称
选择为模拟函数提供一个名称,改名称将在测试错误输出中显示,而不是 jest.fn()
,使用这个可以快速识别在测试输出中报告错误的模拟函数
const myMockFn = jest
.fn()
.mockReturnValue('default')
.mockImplementation(x => x + 42)
.mockName('add42');
Snapshot快照
适合测试配置文件
// xx.js
export const generateConfig = () => {
return {
server: 'http://localhost',
port: 8080
};
};
//xx.test.js
import { generateConfig } from './snopshot';
test('测试 generateConfig 函数', () => {
expect(generateConfig()).toMatchSnapshot();
expect(generateConfig()).toEqual({
server: 'http://localhost',
port: 8080
});
});
jest --watchAll
里面出现了 u
、 i
模式分别对应 更新所有的快照跟更新单个快照
安装 prettier
, 运行 toMatchInlineSnapshot
,会将 快照自动存到代码下面
test('测试 generateConfig 函数', () => {
expect(generateConfig()).toMatchInlineSnapshot(
{
time: expect.any(Date)
},
// 下面是自动生成的
`
Object {
"port": 8080,
"server": "http://localhost",
"time": Any<Date>,
}
`
);
});
ES6 类
// util.js
class Util {
init() {}
a() {
// 异常复杂
}
b() {
// 异常复杂
}
}
export default Util;
在别的函数里面使用这个类
// useUtil.js
import Util from './util';
const useUtil = (a, b) => {
const util = new Util();
util.a(a);
util.b(b);
};
export default useUtil;
写这个使用类的函数的测试用例的时候,我们会发现这个函数因为使用到了类里面的函数,而函数又很复杂,直接调用会损耗性能。所以这里我们用几种方法来模拟
// useUtil.test.js
jest.mock('./util');
// jest.mock 发现 util 是一个类,会自动把类的构造函数方法变成 jest.fn()
// const Util = jest.fn();
// Util.a = jest.fn()
// Util.b = jest.fn()
import Util from './util';
import useUtil from './useUtil';
test('测试 useUtil', () => {
useUtil();
expect(Util).toHaveBeenCalled();
console.log(Util.mock);
expect(Util.mock.instances[0].a).toHaveBeenCalled();
expect(Util.mock.instances[0].b).toHaveBeenCalled();
});
/**
● Console
console.log src/lesson3/useUtil.test.js:12
{ calls: [ [] ],
instances: [ Util { init: [Function], a: [Function], b: [Function] } ],
invocationCallOrder: [ 1 ],
results: [ { type: 'return', value: undefined } ] }
*/
另一种方法就是通过在 __mocks__
文件夹中模拟
// __mocks__/util.js
const Util = jest.fn();
Util.prototype = jest.fn();
Util.prototype = jest.fn();
export default Util;
还有一种写法,是在原来的测试用例修改
// useUtil.test.js
jest.mock('./util', () => {
const Util = jest.fn();
Util.prototype = jest.fn();
Util.prototype = jest.fn();
return Util;
});
DOM操作
// dom.js
import $ from 'jquery'
const addDivToBody = () => {
$('body').append('<div/>')
}
export default addDivToBody
// dom.test.js
// node 本身不具备 dom
// jest 在 node 环境下模拟了一套 dom 的 api,jsDom
import $ from 'jquery'
import addDivToBody from addDivToBody;
test('测试 addDivToBody', () => {
addDivToBody();
expect($('body').find('div').length).toBe(1)
})
TDD(测试驱动开发)
全称:Test Driven Development
开发流程(Red-Green Development)
- 编写测试用例
- 运行测试,测试用例无法通过测试
- 编写代码,使测试用例通过测试
- 优化代码,完成开发
- 重复上述步骤
优势
- 长期减少回归 bug
- 代码质量更好(组织、可维护性)
- 测试覆盖率高
- 错误测试代码不容易出现
Vue TDD
开始
# 安装脚手架
npm i @vue/cli@3.8.4 -g
# 安装vue,可以选择默认配置,也可以自定义配置
vue create vue-jest
@vue/test-utils
// HelloWorld.test.js
import { shallowMount } from '@vue/test-utils';
import HelloWorld from '@/components/HelloWorld.vue';
describe('HelloWorld.vue', () => {
/** 如果不使用 @vue/test-utils
* import Vue from 'vue'
* it('renders props.msg when passed', () => {
const root = document.createElement('div')
root.className = 'root'
document.body.appendChild(root)
new Vue({
render: h => h(HelloWorld, {
props: {
msg: 'laibh'
}
})
}).$mount('.root')
expect(document.getElementsByClassName('hello').length).toBe(1)
})
*/
const msg = 'new message';
const wrapper = shallowMount(HelloWorld, {
propsData: {
msg
}
});
expect(wrapper.text()).toMatch(msg);
});
shallowMount
浅层渲染,只渲染第一层,不渲染子组件,适合单元测试
mount
则会渲染子组件,适合集成测试
开发 Header 组件
测试用例先行
// Header.test.js
import { shallowMount } from '@vue/test-utils';
import Header from '@/components/Header/Header.vue';
describe('测试 Header 组件', () => {
it('Header 包含 Input 框', () => {
const wrapper = shallowMount(Header);
const input = wrapper.find('[data-test="input"]');
expect(input.exists()).toBe(true);
});
it('Header 中 Input 初始内容为空', () => {
const wrapper = shallowMount(Header);
const inputValue = wrapper.vm.$data.inputValue;
expect(inputValue).toBe('');
});
it('Header 中 Input 框值发生变化,值应该也跟着改变', () => {
const wrapper = shallowMount(Header);
const input = wrapper.find('[data-test="input"]');
input.setValue('laibh');
const inputValue = wrapper.vm.$data.inputValue;
expect(inputValue).toBe('laibh');
});
it('Header 中 Input 框输入回车,无内容时无反应', () => {
const wrapper = shallowMount(Header);
const input = wrapper.find('[data-test="input"]');
input.setValue('');
input.trigger('keyup.enter');
expect(wrapper.emitted().add).toBeFalsy();
});
it('Header 中 Input 框输入回车,有内容时向外触发事件,同时清空 inputValue', () => {
const wrapper = shallowMount(Header);
const input = wrapper.find('[data-test="input"]');
input.setValue('laibh');
input.trigger('keyup.enter');
expect(wrapper.emitted().add).toBeTruthy();
expect(wrapper.vm.$data.inputValue).toBe('');
});
});
根据测试用例写代码
// Header.vue
<template>
<div>
<input data-test="input" v-model="inputValue" @keyup.enter="addTodoItem" />
</div>
</template>
<script>
export default {
name: 'Header',
props: {},
data() {
return {
inputValue: ''
};
},
methods: {
addTodoItem() {
if (this.inputValue) {
this.$emit('add', this.inputValue);
this.inputValue = '';
}
}
}
};
</script>
<style scoped lang="less"></style>
测试覆盖率
// jest.config.js
module.exports = {
collectCoverageFrom: ['**/*.{js,vue}', '!**/node_modules/**'],
}
// package.json
"scripts": {
"test:cov": "vue-cli-service test:unit --coverage"
},
React TDD
Enzyme
# 安装
npm i --save-dev enzyme enzyme-adapter-react-16
同样, shallow
适合单元测试, mount
则是集成测试
例子:
// App.je
import React from 'react';
function App() {
return (
// 使用的data-test=xxx 等属性可以做到解耦,不会因为改变样式名而发生改变,另外也不会被hash掉
<div className="app-container" title="laibh" data-test="container">
hello world
</div>
);
}
export default App;
// App.test.js
import React from 'react';
import App from './App';
import Enzyme, { shallow } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
Enzyme.configure({ adapter: new Adapter() });
it('renders without crashing', () => {
const wrapper = shallow(<App />)
// 输出整个内容字符串
console.log(wrapper.debug())
/**
<div className="app-container" title="laibh" data-test="container">
hello world
</div>
*/
expect(wrapper.find('[data-test="container"]').length).toBe(1)
expect(wrapper.find('[data-test="container"]').prop('title')).toBe('laibh')
});
另外它海域一些扩展 API,例如 jest-enzyme,让语法易懂简洁
// App.test.js
import React from 'react';
import App from './App';
import Enzyme, { shallow } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
Enzyme.configure({
adapter: new Adapter()
});
it('renders without crashing', () => {
const wrapper = shallow(<App />);
console.log(wrapper.debug());
const container = wrapper.find('[data-test="container"]');
expect(container.length).toBe(1);
expect(container.prop('title')).toBe('laibh');
// 等同上面两句
expect(container).toExist();
expect(container).toHaveProp('title', 'laibh');
});
别忘记在 jest.config.js
里面进行配置
module.exports = {
setupFilesAfterEnv: ['./node_modules/jest-enzyme/lib/index.js']
};
开发 Header 组件
同样测试先行
import React from 'react';
import Header from '../../index';
import {
shallow
} from 'enzyme';
let wrapper;
let inputElem;
describe('测试 Header 组件', () => {
beforeEach(() => {
wrapper = shallow( < Header / > )
inputElem = wrapper.find('[data-test="input"]')
})
it('正常渲染', () => {
expect(wrapper).toMatchSnapshot()
});
it('包含一个 input', () => {
expect(inputElem).toExist()
});
it('input初始化内容应该为空', () => {
expect(inputElem.prop('value')).toBe('')
});
it('当用户输入时,input内容会跟着变化', () => {
const value = '哈哈哈'
inputElem.simulate('change', {
target: {
value
}
})
expect(wrapper.state('value')).toBe(value)
});
it('当用户输入后,键入回车,如果 input 没有内容,则不操作', () => {
const fn = jest.fn();
const wrapper = shallow( < Header addUndoItem = {
fn
}
/>)
wrapper.setState({
value: ''
}) const inputElem = wrapper.find('[data-test="input"]')
inputElem.simulate('keyUp', {
keyCode: 13
}) expect(fn).not.toBeCalled()
});
it('当用户输入后,键入回车,如果 input 有内容,addUndoItem应该被调用,然后input被清空', () => {
const fn = jest.fn();
const wrapper = shallow( < Header addUndoItem = {
fn
}
/>)
const value = 'haha'; wrapper.setState({
value
}) const inputElem = wrapper.find('[data-test="input"]')
inputElem.simulate('keyUp', {
keyCode: 13
}) expect(fn).toBeCalled() expect(fn).toBeCalledWith(value) const newInputElem = wrapper.find('[data-test="input"]')
expect(newInputElem.prop('value')).toBe('')
});
});
根据测试写代码
import React, { Component } from 'react';
import styles from './index.less';
class Header extends Component {
constructor(props) {
super(props);
this.state = {
value: ''
};
}
handleInputKeyUp = e => {
const { value } = this.state;
const { addUndoItem } = this.props;
if (e.keyCode === 13 && value) {
addUndoItem(value);
this.setState({ value: '' });
}
};
handleInputChange = e => {
this.setState({ value: e.target.value });
};
render() {
const { value } = this.state;
return (
<div className={styles.header}>
<div className={styles.headerContent}>
TodoList
<input
className={styles.headerInput}
data-test="input"
value={value}
onChange={this.handleInputChange}
onKeyUp={this.handleInputKeyUp}
placeholder="Add Todo"
/>
</div>
</div>
);
}
}
export default Header;
TDD 小结
优势:代码质量提高
单元测试
测试覆盖率高,业务耦合度高,代码量大,过于独立
BDD(行为驱动开发)
全称:Behavior Driven Development
集成测试
// vue integration/todoList
import { mount } from '@vue/test-utils';
import TodoList from '../../TodoList';
it(`
1.用户会在 header输入框输入内容
2.用户会点击回车按钮
3.列表项应该增加用户输入内容的列表项
`, () => {
const wrapper = mount(<TodoList />);
const inputElem = wrapper.findAll('[data-test="header-input"]').at(0);
const content = 'haha';
inputElem.setValue(content);
inputElem.trigger('change');
inputElem.trigger('keyup.enter');
const listItems = wrapper.findAll('[data-test="list-item"]').at(0);
expect(listItems.length).toBe(1);
expect(listItems.at(0).text()).toContain(content);
});
// react integration/todoList
import React from 'react';
import { mount } from 'enzyme';
import TodoList from './../../index';
describe('集成测试:TodoList', () => {
let wrapper;
beforeEach(() => {
wrapper = mount(<TodoList />);
});
it(`
1.用户会在 header输入框输入内容
2.用户会点击回车按钮
3.列表项应该增加用户输入内容的列表项
`, () => {
const value = 'haha';
const headerInput = wrapper.find('[data-test="header-input"]');
headerInput.simulate('change', {
target: {
value
}
});
// 按下回车键,keyCode为13
headerInput.simulate('keyUp', {
keyCode: 13
});
// undoListItem
const listItems = wrapper.find('[data-test="list-item"]');
expect(listItems.length).toBe(1);
expect(listItems.at(0).text()).toContain(value);
});
});
TDD 与 BDD 比较
TDD
- 先写测试再写代码
- 一般结合单元测试使用,是白盒测试
- 测试重点在代码
- 安全感低
- 速度快
BDD
- 先写代码再写测试
- 一般结合集成测试使用,是黑盒测试
- 测试重点在 UI (DOM)
- 安全感高
- 速度慢
Redux相关测试
增加 redux 在项目
createStore
// src/store/createStore
import { createStore, combineReducers } from 'redux';
import { reducer as todoReducer } from '../containers/TodoList/store';
const reducer = combineReducers({
todo: todoReducer
});
const store = createStore(reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
export default store;
store/actions.js
import { CHANGE_INPUT_VALUE } from './constants';
export const changeInputValue = value => ({
type: CHANGE_INPUT_VALUE,
value
});
store/constants.js
export const CHANGE_INPUT_VALUE = 'CHANGE_INPUT_VALUE';
store/reducer.js
import { CHANGE_INPUT_VALUE } from './constants';
const initialState = {
inputValue: ''
};
export default (state = initialState, action) => {
switch (action.type) {
case CHANGE_INPUT_VALUE:
return {
inputValue: action.value
};
default:
return state;
}
};
store/index.js
import reducer from './reducer';
import * as actions from './actions';
export { reducer, actions };
TodoList/index.js
import React, { Component } from 'react';
import Header from './../../components/Header/index';
import UndoList from './../../components/UndoList/index';
import styles from './index.less';
class TodoList extends Component {
constructor(props) {
super(props);
this.state = {
undoList: []
};
}
handledeleteItem = index => {
const { undoList } = this.state;
const newList = undoList.filter((item, itemIndex) => itemIndex !== index);
this.setState({ undoList: newList });
};
handleStatusChange = index => {
const { undoList } = this.state;
// undoList.forEach((item, itemIndex) => {
// if (itemIndex === index) {
// Object.assign(item, { status: 'input' })
// return;
// } Object.assign(item, { status: 'div' })
// })
const newList = undoList.map((item, itemIndex) => {
if (itemIndex === index) {
return {
...item,
status: 'input'
};
}
return {
...item,
status: 'div'
};
});
this.setState({ undoList: newList });
};
handleBlur = index => {
const { undoList } = this.state;
const newList = undoList.map(item => {
return {
...item,
status: 'div'
};
});
this.setState({ undoList: newList });
};
handleInputValue = (index, value) => {
const { undoList } = this.state;
const newList = undoList.map((item, itemIndex) => {
if (itemIndex === index) {
return {
...item,
value
};
}
return {
...item
};
});
this.setState({ undoList: newList });
};
addUndoItem = value => {
const { undoList } = this.state;
this.setState({
undoList: [
...undoList,
{
status: 'div',
value
}
]
});
};
render() {
const { undoList } = this.state;
return (
<div className={styles.todoList}>
<Header addUndoItem={this.addUndoItem} />
<UndoList
list={undoList}
deleteItem={this.handledeleteItem}
changeStatus={this.handleStatusChange}
changeBlur={this.handleBlur}
valueChange={this.handleInputValue}
/>
</div>
);
}
}
export default TodoList;
src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import App from './App';
import store from './store/createStore';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
然后修改集成测试的测试用例
src/containers/TodoList/__test__/integration/TodoList.test.js
import React from 'react';
import { mount } from 'enzyme';
import TodoList from './../../index';
// 增加的内容
import { Provider } from 'react-redux';
import store from './../../../../store/createStore';
describe('集成测试:TodoList', () => {
let wrapper;
beforeEach(() => {
wrapper = mount(
<Provider store={store}>
<TodoList />
</Provider>
);
});
it(`
1.用户会在 header输入框输入内容
2.用户会点击回车按钮
3.列表项应该增加用户输入内容的列表项
`, () => {
const value = 'haha';
const headerInput = wrapper.find('[data-test="header-input"]');
headerInput.simulate('change', {
target: {
value
}
});
// 按下回车键,keyCode为13
headerInput.simulate('keyUp', {
keyCode: 13
});
// undoListItem
const listItems = wrapper.find('[data-test="list-item"]');
expect(listItems.length).toBe(1);
expect(listItems.at(0).text()).toContain(value);
});
});
异步代码
compontDidMount
componentDidMount(){
/*
{
data:[
{status:'div',value:'haha'},
],
success:true
}
*/
axios.get('/undoList.json').then(res=>{
this.setState({
undoList:res.data
})
}).catch(e=>{
console.log(e)
})
}
模拟函数
src/__mock__/axios.js
const mockUndoList = {
data: [
{
status: 'div',
value: 'haha'
}
],
success: true
};
export default {
get(url) {
if (url === '/undoList.json') {
return new Promise((resolve, reject) => {
resolve(mockUndoList);
});
}
}
};
测试用例
it(`
1.用户打开页面
2.应该展示接口返回的数据
`,(done)=>{
const wrapper = mount(
<Provider store={store}>
<TodoList />
</Provider>
// 解决数据异步问题
setTimeout(()=>{
wrapper.update();
const listItems = wrapper.find('[data-test="list-item"]')
expect(listItems.length).toBe(1)
done()
},0)
// node的语法
process.nextTick(()=>{
wrapper.update();
const listItems = wrapper.find('[data-test="list-item"]')
expect(listItems.length).toBe(1)
done()
})
)
})
setTimeout
componentDidMount() {
/*
{
data:[
{status:'div',value:'haha'},
],
success:true
}
*/
setTimeout(() => {
axios.get('/undoList.json').then(res => {
this.setState({
undoList: res.data
})
}).catch(e => {
console.log(e)
})
}, 5000)
}
测试用例
jest.useFakeTimers();
it(`
1.用户打开页面
2.应该展示接口返回的数据
`,(done)=>{
const wrapper = mount(
<Provider store={store}>
<TodoList />
</Provider>
expect(setTimeout).toHaveBeenCalledTimes(1);
// 跑完所有 setTimeout 的时间
jest.runAllTimers();
// 解决数据异步问题
setTimeout(()=>{
wrapper.update();
const listItems = wrapper.find('[data-test="list-item"]')
expect(listItems.length).toBe(1)
done()
},0)
)
})
前端自动化测试的优势
- 更好的代码组织,项目的可维护性增强
- 更小的bug 出现概率,尤其是回归测试中的 Bug
- 修改工程质量差的项目,更加安全
- 项目具备潜在的文档特性
- 扩广前端的知识面
Enzyme
Enzyme 是 React 的 JavaScript 测试应用程序,可以轻松测试 React Components 的输出。还可以在给定输出的情况下,遍历以某种方式模拟运行。
主要通过模仿 Jq 用于 DOM 操作和遍历
API
at(index)
.at(index) => shallowWrapper
返回当前 wrapper 中指定索引的节点
const wrapper = shallow(<MyComponent />);
expect(wrapper.find(foo).at(0).props().foo).toEqual('bar');
first()
.first() => ShallowWrapper
将匹配节点集合减少到集合中的第一个,就像 .at(0)
。
expect(wrapper.find(Foo).first().props().foo).to.equal('bar');
last()
.last() => ShallowWrapper
将匹配节点集减少到集合中的最后一个,就像 .at(length - '1)
。'
const wrapper = shallow(<MyComponent />);
expect(wrapper.find(Foo).last().props().foo).to.equal('bar');
childAt(index)
.childAt(index) => ShallowWrapper
返回指定索引的子节点
const wrapper = shallow(<TodoList items={items} />);
expect(wrapper.find('ul').childAt(0).type).toEqual('li');
children([selector])
.children([selector]) => ShallowWrapper
返回父节点某个元素的所有子节点
const wrapper = shallow(<TodoList items={items} />);
expect(wrapper.find('ul').children.length).toEqual(items.length);
closest(selector)
.closest(selector) => shallowWrapper
通过遍历节点祖先,返回第一个相匹配的节点
const wrapper = shallow(<MyComponent />);
expect(wrapper.find(Foo).closest('.bar')).to.have.lengthOf(1);
contains(nodeOrNodes)
.contains(nodeOrNodes) => Boolean
返回所有给定的react元素是否与渲染树中的元素匹配。它将通过检查期望元素是否与包装器元素具有相同的 props 并共享相同的值来确定包装器中的元素是否与预期元素匹配。
let wrapper;
wrapper = shallow(
<div>
<div data-foo="foo" data-bar="bar">
Hello
</div>
</div>
);
expect(
wrapper.contains(
<div data-foo="foo" data-bar="bar">
Hello
</div>
)
).to.equal(true);
expect(wrapper.contains(<div data-foo="foo">Hello</div>)).to.equal(false);
expect(
wrapper.contains(
<div data-foo="foo" data-bar="bar" data-baz="baz">
Hello
</div>
)
).to.equal(false);
expect(
wrapper.contains(
<div data-foo="foo" data-bar="Hello">
Hello
</div>
)
).to.equal(false);
expect(wrapper.contains(<div data-foo="foo" data-bar="bar" />)).to.equal(false);
wrapper = shallow(
<div>
<span>Hello</span>
<div>Goodbye</div>
<span>Again</span>
</div>
);
expect(wrapper.contains([<span>Hello</span>, <div>Goodbye</div>])).to.equal(true);
expect(wrapper.contains([<span>Hello</span>, <div>World</div>])).to.equal(false);
containsAllMatchingElements(patternNodes)
.containsAllMatchingElements(patternNodes) => Boolean
返回所有给定的react元素是否 patternNodes
与包装器的渲染树中的元素匹配。每个元素 patternNodes
必须匹配一次或多次。匹配遵循规则 containsMatchingElement
。
const style = { fontSize: 13 };
const wrapper = shallow(
<div>
<span className="foo">Hello</span>
<div style={style}>Goodbye</div>
<span>Again</span>
</div>
);
expect(wrapper.containsAllMatchingElements([<span>Hello</span>, <div>Goodbye</div>])).to.equal(true);
containsAnyMatchingElements(patternNodes)
.containsAnyMatchingElements(patternNodes) => Boolean
返回至少一个给定的react元素是否 patternNodes
与包装器的渲染树中的元素匹配。一个或多个元素 patternNodes
必须匹配一次或多次。匹配遵循规则 containsMatchingElement
。
const style = { fontSize: 13 };
const wrapper = shallow(
<div>
<span className="foo">Hello</span>
<div style={style}>Goodbye</div>
<span>Again</span>
</div>
);
expect(wrapper.containsAnyMatchingElements([<span>Bonjour</span>, <div>Goodbye</div>])).to.equal(true);
containsMatchingElement(patternNode)
.containsMatchingElement(patternNode) => Boolean
返回 patternNode
react元素是否与渲染树中的任何元素匹配。
const wrapper = shallow(
<div>
<div data-foo="foo" data-bar="bar">
Hello
</div>
</div>
);
expect(
wrapper.containsMatchingElement(
<div data-foo="foo" data-bar="bar">
Hello
</div>
)
).to.equal(true);
expect(wrapper.containsMatchingElement(<div data-foo="foo">Hello</div>)).to.equal(true);
expect(
wrapper.containsMatchingElement(
<div data-foo="foo" data-bar="bar" data-baz="baz">
Hello
</div>
)
).to.equal(false);
expect(
wrapper.containsMatchingElement(
<div data-foo="foo" data-bar="Hello">
Hello
</div>
)
).to.equal(false);
expect(wrapper.containsMatchingElement(<div data-foo="foo" data-bar="bar" />)).to.equal(false);
context([key])
返回包装器根节点的上下文哈希。可选地传入一个props,它将只返回该值。
const wrapper = shallow(<MyComponent />, { context: { foo: 10 } });
expect(wrapper.context().foo).to.equal(10);
expect(wrapper.context('foo')).to.equal(10);
debug([options])
.debug([options]) => String
返回包装器的类似HTML的字符串,以便进行调试。当测试没有通过时,打印到控制台很有用。
options
( Object
[可选]):
- '
options.ignoreProps
:(Boolean
[可选]):是否应在结果字符串中省略props。默认情况下包含道具。' - '
options.verbose
:(Boolean
[可选]):是否应该详细打印作为道具传递的数组和对象。'
dive([options])
.dive([options]) => ShallowWrapper
浅呈现当前包装器的一个非DOM子项,并返回结果周围的包装器。它必须是单节点包装器,并且该节点必须是React组件。
注意:只能在单个非DOM组件元素节点的包装上调用,否则会引发错误。如果必须使用多个子节点对包装器进行浅包装,请使用 .shallow()
function Bar() {
return (
<div>
<div className="in-bar" />
</div>
);
}
function Foo() {
return (
<div>
<Bar />
</div>
);
}
const wrapper = shallow(<Foo />);
expect(wrapper.find('.in-bar')).to.have.lengthOf(0);
expect(wrapper.find(Bar)).to.have.lengthOf(1);
expect(wrapper.find(Bar).dive().find('.in-bar')).to.have.lengthOf(1);
equals(node)
.equals(node) => Boolean
返回当前包装器根节点呈现树是否与传入的树相似
const wrapper = shallow(<MyComponent />);
expect(wrapper.equals(<div className="foo bar" />)).to.equal(true);
every(selector)
.every(selector) => Boolean
返回包装器中的所有节点是否与提供的选择器匹配。
const wrapper = shallow(
<div>
<div className="foo qoo" />
<div className="foo boo" />
<div className="foo hoo" />
</div>
);
expect(wrapper.find('.foo').every('.foo')).to.equal(true);
expect(wrapper.find('.foo').every('.qoo')).to.equal(false);
expect(wrapper.find('.foo').every('.bar')).to.equal(false);
everyWhere(fn)
.everyWhere(fn) => Boolean
const wrapper = shallow(
<div>
<div className="foo qoo" />
<div className="foo boo" />
<div className="foo hoo" />
</div>
);
expect(wrapper.find('.foo').everyWhere(n => n.hasClass('foo'))).to.equal(true);
expect(wrapper.find('.foo').everyWhere(n => n.hasClass('qoo'))).to.equal(false);
expect(wrapper.find('.foo').everyWhere(n => n.hasClass('bar'))).to.equal(false);
exists([selector])
.exists([selector]) => Boolean
返回包装器中是否存在任何节点。或者,如果传入选择器,则该选择器是否在包装器中具有任何匹配项。
const wrapper = mount(<div className="some-class" />);
expect(wrapper.exists('.some-class')).to.equal(true);
expect(wrapper.find('.other-class').exists()).to.equal(false);
filter(selector)
.filter(selector) => ShallowWrapper
返回一个新的包装器,其中只包含与提供的选择器匹配的当前包装器的节点。
const wrapper = shallow(<MyComponent />);
expect(wrapper.find('.foo').filter('.bar')).to.have.lengthOf(1);
filterWhere(fn)
.filterWhere(fn) => ShallowWrapper
返回一个新的包装器,它只包含当前包装器的节点,当传递给提供的谓词函数时,返回true
const wrapper = shallow(<MyComponent />);
const complexFoo = wrapper.find('.foo').filterWhere(n => typeof n.type() !== 'string');
expect(complexFoo).to.have.lengthOf(4);
find(selector)
.find(selector) => ShallowWrapper
查找当前包装器的呈现树中与提供的选择器匹配的每个节点。
import Foo from '../components/Foo';
const wrapper = shallow(<MyComponent />);
expect(wrapper.find('.foo')).to.have.lengthOf(1);
expect(wrapper.find('.bar')).to.have.lengthOf(3);
// compound selector
expect(wrapper.find('div.some-class')).to.have.lengthOf(3);
// CSS id selector
expect(wrapper.find('#foo')).to.have.lengthOf(1);
// 组件
expect(wrapper.find(Foo)).to.have.lengthOf(1);
// 组件显示名称
expect(wrapper.find('Foo')).to.have.lengthOf(1);
// 对象属性选择器
expect(wrapper.find({ prop: 'value' })).to.have.lengthOf(1);
findWhere(fn)
.findWhere(fn) => ShallowWrapper
查找渲染树中为提供的谓词函数返回true的每个节点。
const wrapper = shallow(<MyComponent />);
const complexComponents = wrapper.findWhere(n => n.type() !== 'string');
expect(complexComponents).to.have.lengthOf(8);
forEach(fn)
.forEach(fn) => Self
迭代当前包装器的每个节点,并使用围绕作为第一个参数传入的相应节点的包装器执行提供的函数。
const wrapper = shallow(
<div>
<div className="foo bax" />
<div className="foo bar" />
<div className="foo baz" />
</div>
);
wrapper.find('.foo').forEach(node => {
expect(node.hasClass('foo')).to.equal(true);
});
get(index)
.get(index) => ReactElement
const wrapper = shallow(<MyComponent />);
expect(wrapper.find(Foo).get(0).props.foo).to.equal('bar');
getWrappingComponent()
.getWrappingComponent() => ShallowWrapper
如果 wrappingComponent
传入了a options
,则此方法返回 ShallowWrapper
渲染的周围 wrappingComponent
。这 ShallowWrapper
可以用来更新 wrappingComponent
props,state等。
import { Provider } from 'react-redux';
import { Router } from 'react-router';
import store from './my/app/store';
import mockStore from './my/app/mockStore';
function MyProvider(props) {
const { children, customStore } = props;
return (
<Provider store={customStore || store}>
<Router>{children}</Router>
</Provider>
);
}
MyProvider.propTypes = {
children: PropTypes.node,
customStore: PropTypes.shape({})
};
MyProvider.defaultProps = {
children: null,
customStore: null
};
const wrapper = shallow(<MyComponent />, {
wrappingComponent: MyProvider
});
const provider = wrapper.getWrappingComponent();
provider.setProps({ customStore: mockStore });
getElement()
.getElement() => ReactElement
返回包装的ReactElement。如果当前包装器正在包装根组件,则返回根组件的最新呈现输出。
const element = (
<div>
<span />
<span />
</div>
);
function MyComponent() {
return element;
}
const wrapper = shallow(<MyComponent />);
expect(wrapper.getElement()).to.equal(element);
getElements()
.getElements() => Array<ReactElement>
const one = <span />;
const two = <span />;
function Test() {
return (
<div>
{one}
{two}
</div>
);
}
const wrapper = shallow(<Test />);
expect(wrapper.find('span').getElements()).to.deep.equal([one, two]);
hasClass(className)
.hasClass(className) => Boolean
返回包装节点是否具有 className
包含传入的类名称的prop。它必须是单节点包装器。
const wrapper = shallow(<MyComponent />);
expect(wrapper.find('.my-button').hasClass('disabled')).to.equal(true);
// 正则
expect(wrapper.find('.my-button').hasClass(/(ComponentName)-(other)-(\d+)/)).to.equal(true);
hostNodes()
.hostNodes() => ShallowWrapper
返回仅包含主机节点的新包装器。当使用 react-dom
,主机节点是HTML元素,而不是定制反应的组分
const wrapper = shallow(
<div>
<MyComponent className="foo" />
<span className="foo" />
</div>
);
const twoNodes = wrapper.find('.foo');
expect(twoNodes.hostNodes()).to.have.lengthOf(1);
html()
.html() => String
返回整个当前渲染树(不仅仅是浅渲染部分)的渲染HTML标记的字符串。只能在单个节点的包装器上调用
function Foo() {
return <div className="in-foo" />;
}
function Bar() {
return (
<div className="in-bar">
<Foo />
</div>
);
}
const wrapper = shallow(<Bar />);
expect(wrapper.html()).to.equal('<div class="in-bar"><div class="in-foo"></div></div>');
expect(wrapper.find(Foo).html()).to.equal('<div class="in-foo"></div>');
const wrapper = shallow(
<div>
<b>important</b>
</div>
);
expect(wrapper.html()).to.equal('<div><b>important</b></div>');
instance()
.instance() => ReactComponent
返回单节点包装器节点的底层类实例; this
在它的方法。
function Stateless() {
return <div>Stateless</div>;
}
class Stateful extends React.Component {
render() {
return <div>Stateful</div>;
}
}
test('shallow wrapper instance should be null', () => {
const wrapper = shallow(<Stateless />);
const instance = wrapper.instance();
expect(instance).to.equal(null);
});
test('shallow wrapper instance should not be null', () => {
const wrapper = shallow(<Stateful />);
const instance = wrapper.instance();
expect(instance).to.be.instanceOf(Stateful);
});
is(selector)
.is(selector) => Boolean
返回单个包装节点是否与提供的选择器匹配。它必须是单节点包装器。
const wrapper = shallow(<div className="some-class other-class" />);
expect(wrapper.is('.some-class')).to.equal(true);
isEmptyRender()
.isEmptyRender() => Boolean
返回包装器是否最终只呈现允许的假值: false
或 null
。
function Foo() {
return null;
}
const wrapper = shallow(<Foo />);
expect(wrapper.isEmptyRender()).to.equal(true);
key()
.key() => String
返回当前包装器节点的键值。它必须是单节点包装器。
const wrapper = shallow(
<ul>
{['foo', 'bar'].map(s => (
<li key={s}>{s}</li>
))}
</ul>
).find('li');
expect(wrapper.at(0).key()).to.equal('foo');
expect(wrapper.at(1).key()).to.equal('bar');
map(fn)
.map(fn) => Array<Any>
将当前节点数组映射到另一个数组。每个节点作为a传递 ShallowWrapper
给map函数。
const wrapper = shallow(
<div>
<div className="foo">bax</div>
<div className="foo">bar</div>
<div className="foo">baz</div>
</div>
);
const texts = wrapper.find('.foo').map(node => node.text());
expect(texts).to.eql(['bax', 'bar', 'baz']);
matchesElement(patternNode)
.matchesElement(patternNode) => Boolean
回给定的react元素是否 patternNode
与包装器的渲染树匹配。它必须是单节点包装器,并且仅检查根节点。
这些 patternNode
行为就像一张通配符。为了匹配包装器中的节点:
- '标签名称必须匹配'
- '内容必须匹配:在文本节点中,前导和尾随空格被忽略,但中间空间不被忽略。子元素必须根据这些规则以递归方式匹配。'
- '
patternNode
props(attributes)必须出现在包装器的节点中,而不是相反。如果它们出现,它们的值必须匹配。' - '
patternNode
样式CSS属性必须出现在包装器节点的样式中,而不是相反。如果它们出现,它们的值必须匹配。'
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
// ...
}
render() {
return (
<button type="button" onClick={this.handleClick} className="foo bar">
Hello
</button>
);
}
}
const wrapper = shallow(<MyComponent />);
expect(wrapper.matchesElement(<button>Hello</button>)).to.equal(true);
expect(wrapper.matchesElement(<button className="foo bar">Hello</button>)).to.equal(true);
name()
.name() => String|null
返回此包装器的当前节点的名称。如果它是复合组件,则这将是最顶层渲染组件的名称。如果它是本机DOM节点,则它将是标记名称的字符串。如果是的话 null
,那就是 null
。
返回名称的优先顺序是: type.displayName
- '> type.name
- > type
。'
const wrapper = shallow(<div />);
expect(wrapper.name()).to.equal('div');
function SomeWrappingComponent() {
return <Foo />;
}
const wrapper = shallow(<SomeWrappingComponent />);
expect(wrapper.name()).to.equal('Foo');
Foo.displayName = 'A cool custom name';
function SomeWrappingComponent() {
return <Foo />;
}
const wrapper = shallow(<SomeWrappingComponent />);
expect(wrapper.name()).to.equal('A cool custom name');