iT邦幫忙

2023 iThome 鐵人賽

DAY 10
0
自我挑戰組

為了成為更好的前端,我開始在乎的那些事系列 第 10

[Day 10] 測試思維 & 單元測試 - (6) 單元測試與 Provider

  • 分享至 

  • xImage
  •  

前言

在現今的前端開發中,我們常常利用 Provider 來處理一些全域設定或儲存資料的問題,帶來給我們許多方便,但是,同時也讓我們在前端有使用到 Provider 的 component 變得沒那麼單純,沒那麼像單元測試

此時,我們就需要在每個測試中加入 Provider 的設定,但是也會造成冗長難以閱讀的問題,如接下來的段落所示

所以,今天就會跟大家介紹如和測試有使用 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

https://ithelp.ithome.com.tw/upload/images/20230925/20148944Nzrbebn2vr.png

  • Style Provider

https://ithelp.ithome.com.tw/upload/images/20230925/20148944PqBikpKYTg.png

 

但這時,當我們想要測試單一 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 方法

 

Rewrite default 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 />);
        ...
	});
})

 

Testing 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 }) };
};

 

Usage

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 的過程,使前端的單元測試更好維護和撰寫

 

參考資源


上一篇
[Day 9] 測試思維 & 單元測試 - (5) 如何做好測試? - 可讀篇
下一篇
[Day 11] 測試思維 & 單元測試 - (7) 利用 immer.js 輕鬆建立 mock data
系列文
為了成為更好的前端,我開始在乎的那些事30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言