【Redux Toolkit】 React Testing Library 測試最完整中文指南

閱讀時間約 21 分鐘
Redux with RTL

Redux with RTL

隨著 React 在前端開發中的廣泛應用,我們往往需要進行一系列測試,來確保應用程式的正常運作。

而在 React 測試生態系統中,React Testing Library 成為了方便好用的選擇,因其強調測試應該關注於使用者的操作與觀察元件行為,而不是測試細節實現

本文將分享如何結合 Redux Toolkit 與 React Testing Library,來有效測試 Redux 應用程式。無論你是剛學習測試還是已經熟悉 Redux,希望本文能幫助讀者更了解 Redux Toolkit 和 React Testing Library 的實際用法,讓您的測試寫起來更加輕鬆。

現在,讓我們開始探索這個令人興奮的主題吧!



零、測試的核心原則

在 Redux 官方網站就曾經提到,他們其實是鼓吹「整合測試」。因為多數的情況下,使用者並不在意背後是否使用 Redux,重點在於元件的功能是否正確作動。因此雖然每一個 pure function 一樣可以使用 unit test,但實際上這些內容都能被整合測試覆蓋到。

這裡也推薦兩篇文章,解釋為什麼整合測試特別適合 Redux 測試:


Redux 如何被測試?

React Testing Library 可以被任何的 Test Runner 執行。因此如果使用 CRA 就會預設 Jest;Vite 就會預設為 Vitest 等。而 React Testing Library,就是在模擬 DOM 實際上的顯示與運作。

因此我們可以使用任何的 Test Runner,搭配 Testing Library 來模擬並抓取實際上的 DOM,就能達到測試 React 元件的目的。

在測試的過程中,我們希望實際的邏輯、Action 與 Reducer 都能夠被順利測試,因此我們需要做的方式如下。

  • 將所有的邏輯、Action、Reducer 都使用實際函式。
  • 使用特定導入的 initialState 來模擬初始狀態。
  • 使用 userEvent 來模擬使用者行為。

接著,就讓我們根據上面提到的三個方向,拆解成三個步驟,一步一步整理出完整的測試邏輯吧!




一、設定測試環境

store 設定

store 設定

為了達到這個狀態,我們總共需要設定兩個部分的程式碼,分別是:

  1. 設定 Store 的生成函式
  2. 設定可重複使用 Test Render Function




1.設定 Store 的生成函式

因為我們需要在每一個測試時,都重複生成一個全新的 store,因此我們可以將重新生成的邏輯抽離出來,放在 store 裡面作為備用。

import { combineReducers, configureStore } from '@reduxjs/toolkit'

import cartReducer from '../features/cartSlice'

// 此處我們單純使用 combineReducers,讓我們可以客製化調整 RootState 的種類
const rootReducer = combineReducers({
user: userReducer
})

export const setupStore = preloadedState => {
return configureStore({
reducer: rootReducer,
preloadedState
})
}



2.設定可重複使用的 Render Function

在這個階段,可以透過 Testing Library (後續簡稱 TL)的 render 函式,就像 React 一樣,實際渲染目標的測試元件。但為了避免影響實際 Redux store 的資料,我們需要額外創建一個新的 store 來模擬。

具體的方式,是透過 TL 提供的 render 函式,裡面提供的 Wrapper 屬性,將原本直接渲染在 App.js 裡的 Provider 取代,並模擬傳入的資料。

以下是在 utils 裡程式碼:

// src/utils/test-util.js

import { render } from '@testing-library/react'
import { Provider } from 'react-redux'

// 導入剛剛製作的 store 生成函式
import { setupStore } from '../app/store'

// 導入所需的 Reducer
import cartReducer from '../features/cartSlice'

// 未來會使用的客製化 Render 函式
// render 函式會傳入 ui, options 兩個參數
export function renderWithProviders(
ui, // 此處指的是如 <App /> 等元件
{
// 設定初始化的值
preloadedState = {},
// 當沒有 store 傳入時,自動創建一個新的 store
store = setupStore(preloadedState),
...renderOptions
} = {}
) {
// 建立 Wrapper 元件
function Wrapper({ children }) {
return <Provider store={store}>{children}</Provider>
}

// 將整個 store 和 render 的資料都回傳
return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) }
}


其中,preloadedState = {} 之所以可以在物件裡使用等號,是因為這是解構賦值的預設寫法,當沒有 preloadedState 被傳入時,就將 preloadedState 預設為 {}

到這裡,我們已經完成 Redux Toolkit 的測試前置作業。接著,我們就來開始撰寫我們的測試吧!




二、為元件撰寫整合測試

Test Your Redux with RTL

Test Your Redux with RTL

為了讓大家更完整了解整個 Redux 測試的運作,採用了 Redux 中最重要的兩個環節:

  • 測試資料渲染
  • 測試異步資料獲取

為了更詳細說明如何將測試放到專案中,我們先了解基礎專案的程式碼背景,以及如何導入測試。此處先簡單利用專案部分的程式碼,說明示範的程式碼架構。



專案基礎介紹

該專案是一個簡單電商平台的狀態管理,使用 Redux Toolkit 來管理。主要有 Cart 元件和 cartSlice 這個 store 共兩個文件。

1. Cart 元件介紹

該元件是位於購物車(Cart.js)的元件,主要的作用是將 store 中的 cartItems,透過 map 渲染至 cartItemList,並將元件渲染出來。

const Cart = (props) => {
const isCartShow = useSelector((state) => state.ui.isCartShow);
const cartItems = useSelector((state) => state.cart.cartItems);

const cartItemList = cartItems.map((item) => (
<CartItem
key={item.id}
item={{
id: item.id,
title: item.title,
quantity: item.quantity,
total: item.quantity * item.price,
price: item.price,
}}
/>
));

return {isCartShow && (
<Card className={classes.cart}>
<h2>Your Shopping Cart</h2>
<ul>{cartItemList}</ul>
</Card>
)};
};

export default Cart;



2. cartSlice 介紹

該 Thunk 是將資料從雲端 fetch 後,將資料更新至 cartItems 這個元素中。主要的檔案來自管理購物車(Cart)的 slice,以下是 cartSlice.js 的程式碼:

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'

// 通常 Thunk Creator 會獨自放在 cartAction 這個檔案,但為了簡化就移至 cartSlice 裡
export const fetchOrderData = createAsyncThunk('user/fetchOrderData', async () => {
const response = await fetch('link here...')
return response.data
})

const initialState = {
cartItems: [],
totalQuantity: 0,
status: "idle",
}

const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
addToCart(state, action) {
state.cartItems.push(newItems);
},
removeFromCart(state, action) {
state.cartItems = state.cartItems.filter((item) => item.id !== id);
},
extraReducers: (builder) => {
builder.addCase(fetchCartDataThunk.pending, (state, action) => {
state.status = "loading";
})
.addCase(fetchCartDataThunk.fulfilled, (state, action) => {
state.cartItems = action.payload?.cartItems || [];
state.totalQuantity = action.payload?.totalQuantity || 0;
state.status = "success";
});
},
})

export default userSlice.reducer;


為了不讓程式碼模糊焦點,因此刻意簡化過加入購物車清單中,相同品項的邏輯處理,僅專注在測試結果是否符合預期。

上述的程式碼,主要初始化了用於儲存 cartItems 的陣列、購物車總數 totalQuantity,和目前讀取的狀態 status。

Thunk 一開始會執行 Pending Reducer,將狀態改成 loading。後於 Thunk 完成後,就會觸發後續的 Reducer fulfilled,將資料上傳並更新至 store。




三、RTL 的實際測試流程

接著,我們就可以實際來撰寫我們的測試檔案啦!我們主要會使用的套件有 React Testing LibraryJestMock Service Worker 三個套件。

其中 Mock Service Worker 主要用於模擬 Server 的活動與回應,而不需要真的使用到 Server。可以避免直接使用 Server 導致意外寫入資料,或是讓伺服器擁有額外的壓力。

1. 測試檔案的基礎設定

下面的測試前提,總共做了兩件事,第一件事是設定 Mock Service 預期會獲得的假資料。第二是初始化與強制在每次測試時,重置 Mock Service 的設定。

// 導入 Mock server worker 模擬後端 API
import { rest } from 'msw'
import { setupServer } from 'msw/node'

// 導入 RTL 所需要用的套件
import { screen } from '@testing-library/react'
import { userEvent } from '@testing-library/user-event'

// 使用自己創見的 Render Function 而非 RTL 內建的 Render Function
import { renderWithProviders } from '../../../utils/test-utils'
import Cart from '../Cart'

// 建立 DB 的假資料
const fakeData = { cartItems: [...], totalQuantity: 10 };

// 使用 MSW 來截斷發送的 Request,並模擬回傳的資料
// 使用 delay 讓 loading 狀態可以出現
export const handlers = [
rest.get('fetch link here...', (req, res, ctx) => {
return res(ctx.json(fakeData), ctx.delay(150))
})
]

// 設定初始化 Server
const server = setupServer(...handlers)

// 開啟 Mock Server API
beforeAll(() => server.listen())

// 強制在每一次測試時,都重設 Mock Server 狀態
afterEach(() => server.resetHandlers())

// 在結束測試後關閉 Mock Server API
afterAll(() => server.close())



2. 加入測試程式碼

接著,我們使用 Jest 來撰寫我們的測試。Jest 的測試條件式,是以 test 開頭,並在裡面執行測試 AAA 三步驟。

  • Arrange|設定需要測試的元件
  • Act|執行模擬使用者的行為
  • Assert|斷言測試結果是否與預期相同


實際測試範例

下面的程式碼,主要做的三組測試,分別是:

  1. 確認 fetch 前是否正確顯示 “no item”,且 “Fetching items” 不在元件中。
  2. 確認點擊 Submit 按鈕後,畫面的 “no item” 文字消失。
  3. 等待資料 fetch 完,畫面會顯示正確資料。
test('fetches & receives a user after clicking the fetch user button', async () => {
renderWithProviders(<Cart />)

// 確認存在 "no item",但不存在 "Fetching items" 文字
expect(screen.getByText(/no item/i)).toBeInTheDocument()
expect(screen.queryByText(/Fetching items\\.\\.\\./i)).not.toBeInTheDocument()

// 確認點擊後,不存在 "no item" 文字
userEvent.click(screen.getByRole('button', { name: /submit/i }))
expect(screen.getByText(/no item/i)).toBeInTheDocument()

// 等待資料 fetch 完,顯示第一個物件的名稱,且不存在 "Fetching items"、"no item" 文字
expect(await screen.findByText(/First item/i)).toBeInTheDocument()
expect(screen.queryByText(/no item/i)).not.toBeInTheDocument()
expect(screen.queryByText(/Fetching items\\.\\.\\./i)).not.toBeInTheDocument()
})


看到這裡,可能會有讀者想說,為什麼都沒有針對 Redux 進行測試,而是單純測試畫面內容而已?

那是因為,Redux 的測試邏輯,就是希望開發者不要在乎測試時,背後是否使用 Redux 或任何狀態管理的套件。重點在於畫面「是否顯示」正確的資訊

顯示正確的資訊,才是整合測試中,最重要的關鍵。




如何在元件渲染前測試 store?

如果我需要在元件渲染前,就確定資料有被正確的傳入呢?那我們就可以在客製化後的 Render 函式中,傳入我們預設的 initiateState,並傳入 preloadedState 中。

接著,再從 render 函式中,輸出 getByText 等方法,確保資料可以被正確讀取。當然 find 或 query 等類型的方法,也都能夠從中導出。

test('Uses preloaded state to render', () => {
const initialState = [{ cartItems: [], , status: "idle" }]

const { getByText } = renderWithProviders(<TodoList />, {
preloadedState: {
cart: initialState
}
})
})




RTL 與 Redux 的測試結論

一開始在導入測試時,最不習慣的便是將心態,從「確認函式的返回值」轉換成「實際畫面上的資料」;但以整合測試(Integration Test)作為核心心態,能夠避免雖然 Redux store 運作正常,畫面卻沒有正確顯示的問題。

測試真的是一們很深的學問,無論是如何提升測試覆蓋率,以及在「時間有限」的情況下,哪一些元件需要優先寫測試、哪一些往後放,甚至哪一些僅寫整合測試。

最近也正在研究測試驅動開發(Test-Driven Development),也了解到測試不是所有的功能都寫,而是要反映真正重要,且容易改壞的內容做測試,且以實際結果而非每一個函式來做測試。

如果讀者在閱讀後,也有其他的想法,也歡迎留言或來信交流!



延伸閱讀:

技術文章

學習成長




參考資料:

此處作為整理前端(Frontend)和相關的 HTML、CSS、JavaScript、React 等前端觀念與技巧,全部都會收錄在這個專題之中。同時也會將相關的技術與反思記錄在此,歡迎各位讀者互相交流。
留言0
查看全部
avatar-img
發表第一個留言支持創作者!
在軟體領域中,"Thunk" 是一個常用的術語,它指的是一種用於延遲計算,或將運算延後執行的程式碼片段。它通常用於函數式編程,或編譯器的設計中。Redux 透過 createAsyncThunk 實作了該非同步/異步操作,並提供數個 API 協助我們使用 Redux。
React 表單驗證是一種技術與使用者體驗的設計,讓使用者能夠即時檢查輸入的資料並修正,提升使用者的使用體驗,並確保資料的正確性。
useContext 是一種 React hook,讓我們能夠直接取用其他元件的 Context,而無須層層傳遞 props,進而使程式碼簡潔易讀。
在傳統開發的過程中,很容易會搞混一般的 this 和箭頭函式(arrow function)中的 lexcial "this" 兩者的差異。本文就以實際的例子來說明各自的差異,以及在未來使用時需要注意哪一些細節。
在軟體領域中,"Thunk" 是一個常用的術語,它指的是一種用於延遲計算,或將運算延後執行的程式碼片段。它通常用於函數式編程,或編譯器的設計中。Redux 透過 createAsyncThunk 實作了該非同步/異步操作,並提供數個 API 協助我們使用 Redux。
React 表單驗證是一種技術與使用者體驗的設計,讓使用者能夠即時檢查輸入的資料並修正,提升使用者的使用體驗,並確保資料的正確性。
useContext 是一種 React hook,讓我們能夠直接取用其他元件的 Context,而無須層層傳遞 props,進而使程式碼簡潔易讀。
在傳統開發的過程中,很容易會搞混一般的 this 和箭頭函式(arrow function)中的 lexcial "this" 兩者的差異。本文就以實際的例子來說明各自的差異,以及在未來使用時需要注意哪一些細節。
你可能也想看
Google News 追蹤
Thumbnail
測試對於構建複雜的 Vue 應用至關重要,因為它能防止回歸並鼓勵將應用拆分為可測試的模組。我們介紹了測試的基本術語和推薦的工具,包括單元測試、組件測試和端對端測試。建議越早開始測試,避免隨著時間推移而增加的相依性。單元測試專注於函數和邏輯的正確性,而組件測試則驗證 UI 元素的行為與交互。
Thumbnail
先從react開始: 其實市面上有許多前端框架像是react,angular,vue... 至於為什麼我會選擇react…
Thumbnail
A/B 測試是一種用來測試不同版本效果的實驗方法,可以用於網站優化、電子郵件行銷和社群媒體行銷中。瞭解 A/B 測試的五個大小技巧,包括明確的測試目標、控制變因、足夠的樣本數、一次只測試一個變因以及追蹤長期表現。在進行網頁優化時,可以將 A/B 測試應用於不同標題、文案、等元素,找出有效的改進方向。
React Hooks 是 React 16.8 中引入的一組新的 API,允許你在函數組件中使用狀態和其他 React 特性,而不需要寫類組件。 狀態管理: useState 鉤子允許在函數組件中添加狀態。 副作用管理: useEffect 鉤子允許處理副作用,如數據獲取、訂閱和手動 DO
Thumbnail
在程式任何地方都能修改各種react組件狀態的做法分享
Thumbnail
網站建置後,為了確保優秀的使用者體驗和網站的功能性,進行徹底的後續優化和測試是不可或缺的。以下是建議的重點測試項目: 響應式網頁設計(RWD)測試: 確保網站在各種設備(如桌面電腦、平板和手機)上均展示良好。這包括在不同的屏幕尺寸和解析度上測試,確保網站能夠自如適應不同的顯示需求。
在希臘文中,試煉和試探是同一個字元πειρασμός,但是它的意義是不一樣的。好的試煉會讓人成長,但是試探可能讓人犯錯。在人生中,會面臨到大大小小的試煉和試探。而現代的社會中,試煉和試探又會以各種不同的形式出現。當對他們的認識越清楚,就越能夠知道如何面對或處理。🌿
測試網站和應用程式時需要注意以下事項和執行以下工作: 注意事項: 跨平台相容性: 確保網站或應用程式在各種瀏覽器和設備上的相容性,包括桌面、平板和手機等。 響應式設計測試: 測試網站或應用程式在不同螢幕尺寸和解析度下的表現,確保響應式設計正常運作。 安全性測試: 確保網站或應用程式的安全性,
Thumbnail
Storybook 是一個用來透過獨立元件快速開發 UI 介面的工具,以往要開發元件時,我們可能需要建立一個全新的頁面才能進行開發,但這樣的開發方式可能會有一個狀況:沒有辦法事先開發或是預覽流程中不存在的元件。 透過 Storybook 我們在開發元件時,不需要重新建立複雜的頁面結構⋯⋯
Thumbnail
前言 現在的前端需求已經越來越高,要考慮HTML及CSS的切版美觀程度,以及React以及Flutter所提出的元件(Componet、widget)觀念,也就是將元件模組化,使元件可以更動態的被程式運行,而不用靜態的客製化每一個介面。開發一個好的元件可以提升整體的開發速度,讓任何使用元件的開發者
Thumbnail
測試對於構建複雜的 Vue 應用至關重要,因為它能防止回歸並鼓勵將應用拆分為可測試的模組。我們介紹了測試的基本術語和推薦的工具,包括單元測試、組件測試和端對端測試。建議越早開始測試,避免隨著時間推移而增加的相依性。單元測試專注於函數和邏輯的正確性,而組件測試則驗證 UI 元素的行為與交互。
Thumbnail
先從react開始: 其實市面上有許多前端框架像是react,angular,vue... 至於為什麼我會選擇react…
Thumbnail
A/B 測試是一種用來測試不同版本效果的實驗方法,可以用於網站優化、電子郵件行銷和社群媒體行銷中。瞭解 A/B 測試的五個大小技巧,包括明確的測試目標、控制變因、足夠的樣本數、一次只測試一個變因以及追蹤長期表現。在進行網頁優化時,可以將 A/B 測試應用於不同標題、文案、等元素,找出有效的改進方向。
React Hooks 是 React 16.8 中引入的一組新的 API,允許你在函數組件中使用狀態和其他 React 特性,而不需要寫類組件。 狀態管理: useState 鉤子允許在函數組件中添加狀態。 副作用管理: useEffect 鉤子允許處理副作用,如數據獲取、訂閱和手動 DO
Thumbnail
在程式任何地方都能修改各種react組件狀態的做法分享
Thumbnail
網站建置後,為了確保優秀的使用者體驗和網站的功能性,進行徹底的後續優化和測試是不可或缺的。以下是建議的重點測試項目: 響應式網頁設計(RWD)測試: 確保網站在各種設備(如桌面電腦、平板和手機)上均展示良好。這包括在不同的屏幕尺寸和解析度上測試,確保網站能夠自如適應不同的顯示需求。
在希臘文中,試煉和試探是同一個字元πειρασμός,但是它的意義是不一樣的。好的試煉會讓人成長,但是試探可能讓人犯錯。在人生中,會面臨到大大小小的試煉和試探。而現代的社會中,試煉和試探又會以各種不同的形式出現。當對他們的認識越清楚,就越能夠知道如何面對或處理。🌿
測試網站和應用程式時需要注意以下事項和執行以下工作: 注意事項: 跨平台相容性: 確保網站或應用程式在各種瀏覽器和設備上的相容性,包括桌面、平板和手機等。 響應式設計測試: 測試網站或應用程式在不同螢幕尺寸和解析度下的表現,確保響應式設計正常運作。 安全性測試: 確保網站或應用程式的安全性,
Thumbnail
Storybook 是一個用來透過獨立元件快速開發 UI 介面的工具,以往要開發元件時,我們可能需要建立一個全新的頁面才能進行開發,但這樣的開發方式可能會有一個狀況:沒有辦法事先開發或是預覽流程中不存在的元件。 透過 Storybook 我們在開發元件時,不需要重新建立複雜的頁面結構⋯⋯
Thumbnail
前言 現在的前端需求已經越來越高,要考慮HTML及CSS的切版美觀程度,以及React以及Flutter所提出的元件(Componet、widget)觀念,也就是將元件模組化,使元件可以更動態的被程式運行,而不用靜態的客製化每一個介面。開發一個好的元件可以提升整體的開發速度,讓任何使用元件的開發者