https://jestjs.io/docs/es6-class-mocks
sound-player.js
export default class SoundPlayer {
constructor() {
this.foo = 'bar';
}
playSoundFile(fileName) {
console.log('Playing sound file ' + fileName);
}
}
sound-player-consumer.js
import SoundPlayer from './sound-player';
export default class SoundPlayerConsumer {
constructor() {
this.soundPlayer = new SoundPlayer();
}
playSomethingCool() {
const coolSoundFileName = 'song.mp3';
this.soundPlayer.playSoundFile(coolSoundFileName);
}
}
我们现在来模拟SoundPlayer类
import SoundPlayer from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';
// Automatic mock
jest.mock('./sound-player'); // SoundPlayer is now a mock constructor
beforeEach(() => {
// Clear all instances and calls to constructor and all methods:
SoundPlayer.mockClear();
});
mocks_/sound-player.js
// Import this named export into your test file:
export const mockPlaySoundFile = jest.fn();
const mock = jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
export default mock;
sound-player-consumer.test.js
import SoundPlayer, {mockPlaySoundFile} from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';
jest.mock('./sound-player'); // SoundPlayer is now a mock constructor
beforeEach(() => {
// Clear all instances and calls to constructor and all methods:
SoundPlayer.mockClear();
mockPlaySoundFile.mockClear();
});
it('We can check if the consumer called the class constructor', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
expect(SoundPlayer).toHaveBeenCalledTimes(1);
});
it('We can check if the consumer called a method on the class instance', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
const coolSoundFileName = 'song.mp3';
soundPlayerConsumer.playSomethingCool();
expect(mockPlaySoundFile).toHaveBeenCalledWith(coolSoundFileName);
});
The module factory must be a function that returns a function - a higher-order function (HOF—高阶函数).
import SoundPlayer from './sound-player';
const mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
});
jest.mock()
会被提升到作用域顶层,因此声明的变量 fakePlaySoundFile
不能在 moduleFactory
中使用(否则会报错:使用之前未定义)。但以 mock
开头的变量除外(jest会做额外处理)。
// Note: this will fail
import SoundPlayer from './sound-player';
const fakePlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: fakePlaySoundFile};
});
});
不能返回箭头函数,ES5 doesn't have arrow functions nor classes
jest.mock('./sound-player', () => {
return function () {
return {playSoundFile: () => {}};
};
});
Note: Arrow functions won't work
jest.mock('./sound-player', () => {
return () => {
// Does not work; arrow functions can't be called with new
return {playSoundFile: () => {}};
};
});
Mocking non-default class exports
mock非default exports的class
import {SoundPlayer} from './sound-player';
jest.mock('./sound-player', () => {
// Works and lets you check for constructor calls:
return {
SoundPlayer: jest.fn().mockImplementation(() => {
return {playSoundFile: () => {}};
}),
};
});
import SoundPlayer from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';
jest.mock('./sound-player');
describe('When SoundPlayer throws an error', () => {
beforeAll(() => {
SoundPlayer.mockImplementation(() => {
return {
playSoundFile: () => {
throw new Error('Test error');
},
};
});
});
it('Should throw an error when calling playSomethingCool', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
expect(() => soundPlayerConsumer.playSomethingCool()).toThrow();
});
});
Automatic mock
会使用 mock constructor 替代 ES6 class,使用 mock function(always return undefined
)替代类的方法,因此我们可以调用类的方法(已被替换掉)。Method calls are saved in theAutomaticMock.mock.instances[index].methodName.mock.calls
Manual mock
和 jest.mock(path[, moduleFactory])
都用到了 jest.fn + mockImplementation,需要手动模拟类的属性和方法
后三者都用到mockImplementation
jest.mock(path[, moduleFactory])
方法的完整例子
sound-player-consumer.test.js
import SoundPlayer from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';
const mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
});
beforeEach(() => {
SoundPlayer.mockClear();
mockPlaySoundFile.mockClear();
});
it('The consumer should be able to call new() on SoundPlayer', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
// Ensure constructor created the object:
expect(soundPlayerConsumer).toBeTruthy();
});
it('We can check if the consumer called the class constructor', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
expect(SoundPlayer).toHaveBeenCalledTimes(1);
});
it('We can check if the consumer called a method on the class instance', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
const coolSoundFileName = 'song.mp3';
soundPlayerConsumer.playSomethingCool();
expect(mockPlaySoundFile.mock.calls[0][0]).toEqual(coolSoundFileName);
});
jest.requireActual
Jest允许您在测试用例中模拟整个模块。这对于您测试你的代码是否正常调用某个模块的函数是很有用的。 然而,您有时可能想要在您的 测试文件中只使用部分模拟的模块, 在这种情况下,你想要访问原生的实现,而不是模拟的版本。
考虑为这个 createUser
函数编写一个测试用例:
createUser.js
import fetch from 'node-fetch';
export const createUser = async () => {
const response = await fetch('http://website.com/users', {method: 'POST'});
const userId = await response.text();
return userId;
};
您的测试将要模拟 fetch
函数,这样我们就可以确保它在没有实发出际网络请求的情况下被调用。 但是,您还需要使用一个Response
(包装在 Promise
中) 来模拟fetch
的返回值,因为我们的函数使用它来获取已经创建用户的ID。 因此,您最初可以尝试编写这样的测试用例:
jest.mock('node-fetch');
import fetch, {Response} from 'node-fetch';
import {createUser} from './createUser';
test('createUser calls fetch with the right args and returns the user id', async () => {
fetch.mockReturnValue(Promise.resolve(new Response('4')));
const userId = await createUser();
expect(fetch).toHaveBeenCalledTimes(1);
expect(fetch).toHaveBeenCalledWith('http://website.com/users', {
method: 'POST',
});
expect(userId).toBe('4');
});
但是,如果运行该测试,您将发现createUser
函数将失败,并抛出错误: TypeError: response.text is not a function
。 这是因为您从 node-fetch
导入的 Response
类已经被模拟了(由测试文件顶部的jest.mock
调用) ,所以它不再具备原有的行为。
为了解决这样的问题,Jest 提供了 jest.requireActual
辅助器。 要使上述测试用例工作,请对测试文件中导入的内容做如下更改:
// BEFORE
jest.mock('node-fetch');
import fetch, {Response} from 'node-fetch';
// AFTER
jest.mock('node-fetch');
import fetch from 'node-fetch';
const {Response} = jest.requireActual('node-fetch');
这允许您的测试文件从node-fetch
导入真实的 Response
对象,而不是一个模拟的版本。 意味着测试用例现在将正确通过。
如果JSDOM(web标准的JavaScript实现,用于Node.js)的一些方法在test中没有执行,例如:window.matchMedia()
,Jest returns TypeError: window.matchMedia is not a function
and doesn't properly execute the test.
解决:模拟 window.matchMedia()
In this case, mocking matchMedia
in the test file should solve the issue:
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
如果上述代码段直接运行在要测试的文件中,jest依然会返回相同的错误。此时应该将上述代码抽取到另外的文件(matchMedia.mock.js),且必须被引入在要测试的模块(file-to-test)之前。
import './matchMedia.mock'; // Must be imported before the tested file
import {myMethod} from './file-to-test';
describe('myMethod()', () => {
// Test the method here...
});
https://juejin.cn/post/7003595612977365028#heading-54
在测试框架被安装在环境之后,每个测试文件执行之前,执行所配置的路径的文件。
module.exports = {
setupFilesAfterEnv: ['./jest.setup.js','<rootDir>/setup.js'],
};
.*
.
:匹配除换行符之外的任何单字符*
:匹配前面的子表达式零次或多次$1 :$1是第一个小括号里的内容,$2是第二个小括号里面的内容,依此类推
{
"moduleNameMapper": {
"^@/(.*)$": "<rootDir>/src/$1",
"components/(.*)$": "<rootDir>/src/components/$1",
"module_name_(.*)": "<rootDir>/substituted_module_$1.js",
// 支持数组
"assets/(.*)": [
"<rootDir>/images/$1",
"<rootDir>/photos/$1",
"<rootDir>/recipes/$1"
]
}
}
%stmts是语句覆盖率(statement coverage):是不是每个语句都执行了?
%Branch分支覆盖率(branch coverage):是不是每个if代码块都执行了?
%Funcs函数覆盖率(function coverage):是不是每个函数都调用了?
%Lines行覆盖率(line coverage):是不是每一行都执行了?
https://www.jianshu.com/p/1b7ba84e5a73
https://www.jianshu.com/p/b271e1395167
search type (单个元素) | search type (多个元素) | function (查询单个元素) | 适用场景 |
---|---|---|---|
getBy | getAllBy | getByText getByRole getByLabelText getByPlaceholderText getByAltText getByDisplayValue | 由于只返回元素或error,适用于确定该元素存在的情况 |
queryBy | queryAllBy | queryByText queryByRole queryByLabelText queryByPlaceholderText queryByAltText queryByDisplayValue | 用于元素可能不存在 |
findBy | findAllBy | findByText findByRole findByLabelText findByPlaceholderText findByAltText findByDisplayValue | 用于异步元素 |
参数可以是字符串或正则,如果参数是字符串,则默认是精确匹配(exact match)。
LabelText: getByLabelText: <label for="search" />
PlaceholderText: getByPlaceholderText: <input placeholder="Search" />
AltText: getByAltText: <img alt="profile" />
DisplayValue: getByDisplayValue: <input value="JavaScript" />
TestId: getByTestId: <div data-testid='search'></div>
区别是:getByText
找不到元素时,会直接抛异常,test case 会随之中断报错;而 queryByText
在这种情况下是返回 null
,所以 queryBy
常用于断言某个元素不存在于 Document 中。这种测试挺常见的,比如传个参数到组件里让它隐藏掉某个元素。
The getByRole
function is usually used to retrieve elements by aria-label attributes. However, there are also implicit roles on HTML elements -- like button for a button element. (某些元素已经隐式地含有role)
A neat feature of getByRole
is that it suggests roles if you provide a role that's not available.(如果你设置的role无效,这里有些建议)
The neat thing about getByRole
: it shows all the selectable roles if you provide a role that isn't available in the rendered component's HTML:(如果你设置的role无效,将会展示可选的role供你设置)
// App.jsx
...
<input
id="search"
type="text"
value={value}
onChange={onChange}
/>
...
这里 <input>
标签的 role 是 textbox
(不知道 role 是啥?没关系,getByRole(瞎写一个)
,控制台会告诉你的)
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
describe('App', () => {
test('renders App component', () => {
render(<App />);
screen.getByRole('');
});
});
This means that the previous test outputs the following to the command line after running it:
Unable to find an accessible element with the role ""
Here are the accessible roles:
document:
Name "":
<body />
--------------------------------------------------
textbox:
Name "Search:":
<input
id="search"
type="text"
value=""
/>
--------------------------------------------------
expect(screen.getByRole('textbox')).toBeInTheDocument();
So quite often it isn't necessary to assign aria roles to HTML elements explicitly for the sake of testing, because the DOM already has implicit(隐式) roles attached to HTML elements.
是异步函数;用在一些需要异步渲染的元素测试上
异步元素
function getUser() {
return Promise.resolve({ id: '1', name: 'Robin' });
}
function App() {
const [search, setSearch] = React.useState('');
const [user, setUser] = React.useState(null);
React.useEffect(() => {
const loadUser = async () => {
const user = await getUser();
setUser(user);
};
loadUser();
}, []);
function handleChange(event) {
setSearch(event.target.value);
}
return (
<div>
{user ? <p>Signed in as {user.name}</p> : null}
<Search value={search} onChange={handleChange}>
Search:
</Search>
<p>Searches for {search ? search : '...'}</p>
</div>
);
}
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
describe('App', () => {
test('renders App component', async () => {
render(<App />);
expect(screen.queryByText(/Signed in as/)).toBeNull();
screen.debug();
expect(await screen.findByText(/Signed in as/)).toBeInTheDocument();
screen.debug();
});
});
它可以用于查询处于
accessibility tree
中的所有元素,并且可以通过name
选项过滤返回的元素。它应该是你查询的首选项。大多数情况下,它都会带着name
选项一起使用,就像这样:getByRole('button', {name: /submit/i})
。这里是Roles的列表可供参考Roles list
这是查询表单元素的首选项
这是查询表单元素的一个代替方案
它对表单没有用,但这是找到大多数非交互式元素(例如div和spans)的第一方法
getByDisplayValue
getByAltText
如果你查询的元素支持
alt
text(例如img
area
input
),那么可以使用它来进行查询
通过title属性查询,但是注意,一般通过屏幕查看页面的用户是无法直接看到titile的
它通常用于查询一些用户看不到听不到的内容,这些内容无法通过Role或者Text匹配到。
扩展断言
//引入
import '@testing-library/jest-dom';
//Custom matchers
toBeDisabled
toBeEnabled
toBeEmptyDOMElement
toBeInTheDocument
toBeInvalid
toBeRequired
toBeValid
toBeVisible
toContainElement
toContainHTML
toHaveAccessibleDescription
toHaveAccessibleName
toHaveAttribute
toHaveClass
toHaveFocus
toHaveFormValues
toHaveStyle
toHaveTextContent
toHaveValue
toHaveDisplayValue
toBeChecked
toBePartiallyChecked
toHaveErrorMessage
React Testing Library(以下简称RTL)
它为 RTL 提供了一整套用户操作集: 除了type
,还包括如下几种,大家有空可以试一下,都非常直观。
模拟请求接口
https://zh-hans.reactjs.org/docs/test-renderer.html
testRenderer.toJSON()
testRenderer.toTree()
testRenderer.update()
testRenderer.unmount()
testRenderer.getInstance()
testRenderer.root
testInstance.find()
testInstance.findByType()
testInstance.findByProps()
testInstance.findAll()
testInstance.findAllByType()
testInstance.findAllByProps()
testInstance.instance
testInstance.type
testInstance.props
testInstance.parent
testInstance.children
基础教程:https://www.robinwieruch.de/react-testing-library/
Unmounts React trees that were mounted with render.
如果测试框架支持全局属性afterEach
(like mocha, Jest, and Jasmine),则会自动执行,不需额外手动引入。
对react-dom/test-utils中的act函数进行了一层包装。act的作用是在你进行断言之前,保证所有由组件渲染、用户交互以及数据获取产生的更新全部在dom实现。
act(() => {
// render components
});
act(() => {
// fireEvent
});
// make assertions
jest.mock()
and jest.requireActual()
to partial mock react
module. It means you only need to mock useRef
hook, keep others as original.jest.mocked
to make your TS types correctly.Object.defineProperty()
to define the setter and getter methods of deviceRef
. We will add the spy to the addEventListener
method of the current element in the setter.jest.resetAllMocks()
to reset partial mocked react
module to original version after testing.import React, { useState, useRef, useEffect } from 'react';
interface Props {}
export enum EVENT_TYPE {
MOUSEOVER = 'MOUSEOVER',
MOUSEOUT = 'MOUSEOUT',
}
export const DeviceModule = (props: Props) => {
const [isTooltipVisible, changeTooltipVisibility] = useState(false);
const deviceRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (deviceRef && deviceRef.current) {
deviceRef.current.addEventListener(EVENT_TYPE.MOUSEOVER, () => changeTooltipVisibility(true));
deviceRef.current.addEventListener(EVENT_TYPE.MOUSEOUT, () => changeTooltipVisibility(false));
}
return () => {
if (deviceRef && deviceRef.current) {
deviceRef.current.removeEventListener(EVENT_TYPE.MOUSEOVER, () => changeTooltipVisibility(true));
deviceRef.current.removeEventListener(EVENT_TYPE.MOUSEOUT, () => changeTooltipVisibility(false));
}
};
});
return <div ref={deviceRef}>my device module</div>;
};
import React, { useRef } from 'react';
import { mount } from 'enzyme';
import { DeviceModule, EVENT_TYPE } from './';
jest.mock('react', () => {
const originReact = jest.requireActual('react');
return {
...originReact,
useRef: jest.fn(),
};
});
const mUseRef = jest.cked(useRef);
describe('66561050', () => {
afterAll(() => {
jest.resetAllMocks();
});
it('should add event listener for device ref and do cleanup work when component unmount', () => {
const mRef = { current: {} };
let addEventListenerSpy!: jest.SpyInstance;
let removeEventListenerSpy!: jest.SpyInstance;
Object.defineProperty(mRef, 'current', {
get() {
return this._current;
},
set(current) {
if (current) {
addEventListenerSpy = jest.spyOn(current, 'addEventListener');
removeEventListenerSpy = jest.spyOn(current, 'removeEventListener');
}
this._current = current;
},
});
mUseRef.mockReturnValueOnce(mRef);
const wrapper = mount(<DeviceModule />);
expect(addEventListenerSpy).toBeCalledWith(EVENT_TYPE.MOUSEOVER, expect.any(Function));
expect(addEventListenerSpy).toBeCalledWith(EVENT_TYPE.MOUSEOUT, expect.any(Function));
wrapper.unmount();
expect(removeEventListenerSpy).toBeCalledWith(EVENT_TYPE.MOUSEOVER, expect.any(Function));
expect(removeEventListenerSpy).toBeCalledWith(EVENT_TYPE.MOUSEOUT, expect.any(Function));
});
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。