我创建了一个自定义挂钩来显示通知消息,并在2秒钟后将其删除。我想为此写一个测试。我刚开始写测试,不完全确定如何为此编写测试。有人能帮我吗?
我的挂钩
export function useNotification() {
const dispatch = useDispatch();
const notifications = useSelector(getNotificationsState);
function toast(type, message) {
const id = notifications.toasts.length;
const data = { type, message, id };
dispatch({
type: NOTIFICATION_ACTIONS.ADD_TOAST,
payload: data,
});
setTimeout(() => {
dispatch({
type: NOTIFICATION_ACTIONS.REMOVE_TOAST,
payload: id,
});
}, NOTIFICATION_TIMEOUT);
}
return toast;
}
我想写挂钩的测试
describe('useNotification', () => {
//Actual test
});
希望这个片段能帮助
import { useSelector, useDispatch } from 'react-redux';
jest.useFakeTimers();
const mockedUseDispatch = jest.fn();
const mockedUseSelector = jest.fn();
jest.mock('react-redux', () => ({
useSelector: mockedUseSelector,
useDispatch: mockedUseDispatch
}));
describe('useNotification', () => {
it('Should show and dismiss toast', () => {
//arrange
useSelector.mockImplementation((getNotificationsState) => getNotificationsState(yourMockedStoreData));
//act
const toast = useNotification()
toast(type, message) //type, message are your own data
//assert
expect(toast).toHaveBeenCallTimes(1)
expect(toast).toHaveBeenCallWith({
type: NOTIFICATION_ACTIONS.ADD_TOAST,
payload: data, //your expected data
})
// Fast-forward
jest.advanceTimersByTime(NOTIFICATION_TIMEOUT);
expect(toast).toHaveBeenCallTimes(2)
expect(toast).toHaveBeenCallWith({
type: NOTIFICATION_ACTIONS.ADD_TOAST,
payload: id, //your expected data
})
})
});
请注意,我无法在您的设置中运行它,所以我试图让您基本了解如何实现自定义钩子的单元测试。
一些参考
https://jestjs.io/docs/timer-mocks#advance-计时器按时间
https://dev.to/coderjay06/the-three-a-s-of-unit-testing-b22
不要模拟第三方库。模拟实现将破坏它们的功能。在您的案例中,如果只使用jest.fn()
来模拟useSelector
和useDispatch
钩子,那么它们的函数就会被破坏。
看看useSelector
钩子的原始实现,你不能简单地为它提供一个mock实现。它有验证逻辑、上下文切换逻辑,并在内部调用useSyncExternalStoreWithSelector
钩子来订阅redux store的更改。
这就是为什么我们应该测试useSelector
钩子的功能,而不是它的实现。实现经常发生变化,而测试实现可能会使测试代码变得脆弱。
相反,我们可以测试React自定义钩子的功能,而不是实现。
在这里,我们可以使用react钩子测试库来测试react自定义钩子。我们还使用redux中的createStore
函数创建了一个模拟存储进行测试。
例如
useNotification.ts
:
import { useDispatch, useSelector } from "react-redux";
export const getNotificationsState = state => state.notifications;
export const NOTIFICATION_ACTIONS = {
ADD_TOAST: 'ADD_TOAST',
REMOVE_TOAST: 'REMOVE_TOAST'
}
export const NOTIFICATION_TIMEOUT = 3000;
export function useNotification() {
const dispatch = useDispatch();
const notifications = useSelector(getNotificationsState);
function toast(type, message) {
const id = notifications.toasts.length;
const data = { type, message, id };
dispatch({ type: NOTIFICATION_ACTIONS.ADD_TOAST, payload: data });
setTimeout(() => {
dispatch({ type: NOTIFICATION_ACTIONS.REMOVE_TOAST, payload: id });
}, NOTIFICATION_TIMEOUT);
}
return toast;
}
useNotification.test.tsx
:
import { renderHook, act } from "@testing-library/react-hooks";
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import { NOTIFICATION_ACTIONS, NOTIFICATION_TIMEOUT, useNotification } from "./useNotification";
import React from "react";
describe('useNotification', () => {
beforeAll(() => {
jest.useFakeTimers();
})
afterAll(() => {
jest.useRealTimers();
})
test('should pass', () => {
const rootReducer = (state = { notifications: { toasts: [] as any[] } }, action) => {
switch (action.type) {
case NOTIFICATION_ACTIONS.ADD_TOAST:
return { ...state, notifications: { toasts: [...state.notifications.toasts, action.payload] } }
case NOTIFICATION_ACTIONS.REMOVE_TOAST:
return { ...state, notifications: { toasts: state.notifications.toasts.filter(t => t.id !== action.payload) } }
default:
return state;
}
}
const store = createStore(rootReducer);
const wrapper = ({ children }) => <Provider store={store}>{children}</Provider>;
const { result } = renderHook(useNotification, { wrapper });
act(() => {
result.current('a', 'ok');
})
expect(store.getState()).toEqual({ notifications: { toasts: [{ message: 'ok', type: 'a', id: 0 }] } });
act(() => {
jest.advanceTimersByTime(NOTIFICATION_TIMEOUT);
})
expect(store.getState()).toEqual({ notifications: { toasts: [] } });
})
});
测试结果:
PASS stackoverflow/71351220/useNotification.test.tsx (19.078 s)
useNotification
✓ should pass (29 ms)
--------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
useNotification.ts | 100 | 100 | 100 | 100 |
--------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 19.804 s
软件包版本:
"@testing-library/react-hooks": "^8.0.1",
"jest": "^26.6.3",
"react": "^16.14.0",
"react-redux": "^7.2.2",