React Jest Testing Library 測試 JavaScript

React 搭配 Testing Library 實作測試

黃彥鈞 Jim Huang 2023/09/04 10:15:36
970

本篇使用的 JavaScript 框架是 Jest,所以在開始前要先了解一些 Jest 基本語法,才會比較快理解喔!

 

Testing Libary

雖然 Jest 本身有提供很多測試方法,但在測試上都比較偏向邏輯測試,像是 a + b 是否等於 c。而實際上我們所需要的測試有一大部分也包含 UI 的測試,像是畫面上有沒顯示正確文字,或是使用者點擊有沒有跳出視窗等等,這時候就是 Testing Library 出馬的時候了!

它的官網介紹就很明確地告訴你

The @testing-library family of packages helps you test UI components in a user-centric way.

也就是 @testing-library 系列的套件可以幫助你以用戶為中心的方式測試 UI 元件。

Testing Library 不只提供可以共通使用的測試套件,還有專為各個框架所出的套件,基本的三大框架 React、Vue、Angular 都有屬於他們的測試 UI 套件。而比較新的框架像是 Preact、Svelte 也都有在持續更新。

 

Testing Libaray For React

以 React 來說,搭配 Jest 會需要的 Testing Libary 有以下幾個:

1. @testing-library/react - 模擬 React 渲染,Ex:render、screen

2. @testing-library/jest-dom - 擴充 jest 的斷言庫,Ex:toBeInTheDocument、toHaveClass

3. @testing-library/user-event - 模擬使用者操作,Ex:useEvent.click、userEvent.type

接下來來各別介紹這三個的使用時機:

◆ @testing-library/react

主要是提供模擬渲染 UI 畫面的函式,比較常用的有

1. render()

就如同 React 中的 render,就是模擬渲染 React 元件,假設有一個 <Home/> 元件:

export const Home = () => {
  return <h1>Home Page</h1>
};

要測試元件的 DOM 元素時,就可以使用 render() 函式。

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

describe('testing home component', () => {
  it('show Home Page in the home component', () => {
    const {getByText} = render(<Home/>);
    expect(getByText('Home Page')).toBeTruthy(); //使用 getByText 判斷抓到文字的元素是否存在
  });
});

render() 函式會回傳一個物件包含一些屬性對象:

  • ...Query:getBy、queryBy、findBy、getAllBy、queryAllBy、findAllBy,取得元件內的元素。

  • container:渲染的 DOM 節點。

  • debug:偵錯函式,可以顯示當前的 DOM 結構。

  • rerender:重新渲染元件。

  • unmount:取消渲染。

Query 有很多不同的取元素方法,有 get、find、query,各自都有取得多元素的 all 方法

Query 種類\結果

沒有符合

一項符合 大於一個符合 是否為非同步函式
getBy... Throw error

回傳元素

Throw error
queryBy... 回傳 null

回傳元素

Throw error
findBy... Throw error

回傳元素

Throw error
getAllBy... Throw error

回傳陣列元素

回傳陣列元素
queryAllBy... 回傳 []

回傳陣列元素

回傳陣列元素
findAllBy... Throw error

回傳陣列元素

回傳陣列元素

 

看起來很容易搞混,不過他們都有各自使用的時機

  • getBy...:大部分都可以使用,判斷元素是否存在。

  • queryBy...:因為找不到會回傳 null 的特性,所以常用來判斷元素是否一開始不存在。

  • findBy...:非同步函式,可以判斷需等待的元素是否存在,例如 API 回傳才會顯示在畫面上。

2. screen()

雖然 render() 解決了模擬 UI 的情況,不過只能根據 render() 的內容進行測試,如果有多個 render() 函式測起來就會比較麻煩,這時候就可以使用 screen()

其實 screen() 算是 @testing-library/dom 所提供,而所有框架的 @testing-library 底層都有 @testing-library/dom,所以這邊才可以直接做使用。

screen() 所抓取的元素是 <body></body> 內的所有 DOM 元素。

import { render, screen } from '@testing-library/react'

describe('testing home component', () => {
  it('show Home Page in the home component', () => {
    render(<Home/>);
    render(<Home/>);
    expect(getAllByText('Home Page')).toBeTruthy(); //使用 getAllByText 一次判斷兩個 Home 元件元素是否存在
  });
});

個人在抓元素時是比較常用  screen() 勝過於使用 render() 回傳的方法,使用起來也比較方便。

3. renderHook()

顧名思義就是去模擬客製化的 React Hook,假設今天有一個計數器的 customhook

import { useState } from 'react';

function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);

  const increment = () => {
    setCount(count + 1);
  };

  const decrement = () => {
    setCount(count - 1);
  };

  return { count, increment, decrement };
}

export default useCounter;

要測試這個 hook 就可以使用 renderHook(),他可以傳入兩個參數,第一個就是要測試的函式,第二是傳入函式的參數 (非必要)。

import useCounter from './useCounter';
import { renderHook, act } from '@testing-library/react';

it("should increment and decrement the counter", () => {
  const { result } = renderHook(useCounter);

  expect(result.current.count).toBe(0);

  act(() => {
    result.current.increment(); // 呼叫 + 1 函式
  });

  expect(result.current.count).toBe(1);

  act(() => {
    result.current.decrement(); // 呼叫 -1 函式
  });

  expect(result.current.count).toBe(0);
});

renderHook() 會回傳一個物件,可以使用 result 屬性利用 result.current 去操控函式的回傳物件。

如果有更改 state 的操作,就需要使用 act 包起來,這樣才可以即時更新 state 的狀態。

 

◆ @testing-library/jest-dom

@testing-library/jest-dom 提供給 Jest 很多 DOM 元素的擴充判斷,讓我們在抓元素時,有更多的方法去測試,比較常用的像是 toBeInTheDocumenttoHaveClass 等等。

以剛剛的 <Home/> 渲染測試為例

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

describe('testing home component', () => {
  it('show Home Page in the home component', () => {
    const {getByText} = render(<Home/>);
    expect(getByText('Home Page')).toBeInTheDocument(); //判斷元素是否存在於 Document
  });
});

或是

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

describe('testing div', () => {
  it('test div classname is hide', () => {
    render(<div className='hide'>test</div>);
    expect(screen.getByText('test')).toHaveClass('hide'); //判斷元素是否含有指定的 class name
  });
});

 

◆ @testing-library/user-event

最後一個就是模擬使用著操作,user-event 常常跟另一個 @testing-library/react 的 fireEvent 拿來比較,fireEvent 就是在程式碼中會用的事件處理,像是 click 或是 change,而 user-event 的底層就是 fireEvent,不過 userEvent 能更貼合使用者的模擬情況,像是同樣是在 input 輸入文字,如果使用 fireEvent 就會使用 change 的事件。

fireEvent:

import { fireEvent } from '@testing-library/react';

fireEvent.change(inputElement, { target: { value: 'Hello, world!' } });

不過如果是用 userEvent 就會使用 type 的事件。

userEvent:

import userEvent from '@testing-library/user-event';

const user = userEvent.setup()
user.type(inputElement, 'Hello, world!');

乍看之下沒有什麼不一樣,不過 userEvent 的 type 還包含使用者點擊 input,輸入文字的 keydown、keyup 事件,比起 fireEvent 的 change,更貼近實際的使用者操作情況。

除此之外,userEvent 還有提供很多實用的方法像是:

  • dblClick:點擊兩次

  • tripleClick:點擊三次

  • type:input 輸入

  • upload:上傳

  • hover/unhover:指標移進移出

  • copy/paste:複製/貼上

詳細的可以參考 User Interactions

黃彥鈞 Jim Huang