在現今的前端開發中,我們常常利用 Provider 來處理一些全域設定或儲存資料的問題,帶來給我們許多方便,但是,同時也讓我們在前端有使用到 Provider 的 component 變得沒那麼單純,沒那麼像單元測試
此時,我們就需要在每個測試中加入 Provider 的設定,但是也會造成冗長難以閱讀的問題,如接下來的段落所示
所以,今天就會跟大家介紹如和測試有使用 Provider 的工作單位,和與 Provider 測試,更加清晰簡潔的寫法
在單元測試中,我們只想要測試單一元件,但是在此元件中,我們可能會用到不同 Provider 提供的狀態 / 設定,我們測試時,就需要另外將此元件包在 Providers 中,例如
Redux
import { Provider } from 'react-redux';
<Provider store={store}>
<TargetComponent />
</Provider>
Style Provider
import { TonicProvider } from '@tonic-ui/react';
<TonicProvider>
<TargetComponent />
</TonicProvider>
不然會產生以下的錯誤
Redux
Style Provider
但這時,當我們想要測試單一 Component 時,我們在每個 test case 都需要這樣包裝,造成
import { Provider } from 'react-redux';
import { TonicProvider } from '@tonic-ui/react';
import { render } from '@testing-library/react';
describe('TargetComponent', () => {
describe('when under some scenaior', () => {
it('should perform some behavior', () => {
// Arrange
const store = createReduxStore();
// Act
const { getByTestId } = render(
<Provider store={store}>
<TonicProvider>
<TargetComponent />
</TonicProvider>
</Provider>
);
const senderEmailInput = getByTestId('tagetId');
// Assert
expect(senderEmailInput).toHaveValue('benson_chen@trendmicro.com');
});
});
});
想像一下如果我們每一個 test case 都需要這樣撰寫,那會是非常麻煩的步驟,
這也違反了 AOUT 中 Ch.8 提到 好的單元測試 的準則,包括
可讀性 ( Readability )
被測試的 Component 被多層 Provider 包覆,不易閱讀
可維護性 ( Maintainability )
撰寫和改寫都不方便,會造成轉寫時程長,大家不想維護
因此,React Testing Library 的作者 Kent C Dodds. 和 Redux 官網推薦了一些改善的方案
主要的核心方法就是:改寫原本的
render
方法
render
我們先將所有的 Providers 都集合在一起,然後再客製化 React Testing Library 的 render
function,將 Providers 傳入 render
第 2 個參數,做一些初始化設定
import React from 'react';
import { render } from '@testing-library/react';
// example Providers
import { ThemeProvider } from 'my-ui-lib'
import { TranslationProvider } from 'my-i18n-lib'
import defaultStrings from 'i18n/en-x-default'
const AllTheProviders = ({children}) => {
return (
<ThemeProvider theme="light">
<TranslationProvider messages={defaultStrings}>
{children}
</TranslationProvider>
</ThemeProvider>
)
};
const customRender = (ui, options) => (
render(ui, { wrapper: AllTheProviders, ...options });
)
// re-export everything
export * from '@testing-library/react';
// override render method
export { customRender as render };
這時,我們就可以使用原本 Provider 提供的 api,而不需要另外多包一層 Provider 了
// targetComponent.jsx
const TargetComponent = () => {
const { color } = useTheme();
return (
<Box color={color.blue}>
I'm target
</Box>
);
};
// targetComponent.test.jsx
describe('TargetComponent', () => {
test('when under some scenario, should perform some behaviors', () => {
...
const { getByTestId, debug } = render(<TargetComponent />);
...
});
})
render
with Redux
Redux 也可以採用類似的做法,
我們可以將偽造的 redux state 傳入 render
的第 2 個參數 option
中,
( option 本來就可以傳入一些預設值,像是 initialProps,所以我覺得把 mock redux state 放在這裡是合理的 )
import React from 'react'
import { render } from '@testing-library/react'
import { configureStore } from '@reduxjs/toolkit'
import { Provider } from 'react-redux'
// As a basic setup, import your same slice reducers
import userReducer from '../features/users/userSlice'
const renderWithRedux = (
ui: ReactElement,
{
mockReduxState = {},
// Automatically create a store instance if no store was passed in
store = setupStore(mockReduxState),
...renderOptions
}: ExtendedRenderOptions = {},
) => {
const Wrapper = function Wrapper({ children }) {
return (
<Provider store={store}>
<UiProviders>
{children}
</UiProviders>
</Provider>
);
};
return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) };
};
const mockReduxState = {
...state,
account: {
...state.account,
profile: {
...state.account.profile,
email: 'test@testdomain.com',
},
},
};
const { getByTestId, debug } = renderWithRedux(
<TargetComponent />, { mockReduxState }
);
這樣,我們在撰寫測試程式碼時,就可以不用在 render 的時候還要去包覆多個 Provider,讓測試程式碼看起來更容易閱讀了!!
前端的 Provider 方式雖然解決了很多 React 樹狀結構的節點需共享狀態的問題,但也造成單元測試上很大的困難,我們可以利用預設帶有一些常用的 Provider,來簡化撰寫 Unit test 的過程,使前端的單元測試更好維護和撰寫