測試你的 React App

tags: React Testing
category: Front-End
description: 測試你的 React App
created_at: 2022/01/15 13:00:00

cover image


前言

這裡假設用最基本的 Counter 當作測試用的範例


事前準備

  • 裝好 Node.js

先建立好 React 專案

$ npx create-react-app my-app

基本指令

  • test(測試描述[string], 測試主體[function])
  • render(元件[JSX])
  • screen 相關函數
  • expect 相關函數
  • fireEvent 相關函數

先看看 React 預設的 test file

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

test('renders learn react link', () => {
  render(<App />);
  const linkElement = screen.getByText(/learn react/i);
  expect(linkElement).toBeInTheDocument();
});

如果你去跑測試

$ npm test

or

$ npm run test

or yarn

yarn test

會看到類似這樣的東西

PASS  src/App.test.js
  √ renders learn react link (29 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.275 s
Ran all test suites.

Watch Usage
 › Press f to run only failed tests.
 › Press o to only run tests related to changed files.
 › Press q to quit watch mode.
 › Press p to filter by a filename regex pattern.     
 › Press t to filter by a test name regex pattern.    
 › Press Enter to trigger a test run.

可以發現說其實 renders learn react link 就是 test 函數的第一個參數,那下面來說明測試主體做了哪些事

render(<App />); // 渲染 <App /> 組件
const linkElement = screen.getByText(/learn react/i); // 從全域的 screen 變數去取得 element , 這邊使用的是 ByText 就是從文字去抓,可以支援 Regex
expect(linkElement).toBeInTheDocument(); // 預期 linkElement 有存在

如果你把它的 Text 亂改一下,例如:

const linkElement = screen.getByText(/learn react!!/i);

再跑測試就會看到一大串錯誤,像是下面這樣

 FAIL  src/App.test.js
  × renders learn react link (36 ms)

  ● renders learn react link

    TestingLibraryElementError: Unable to find an element with the text: /learn react!!/i. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.

    Ignored nodes: comments, <script />, <style />
    <body>
      <div>
        <div
          class="App"
        >
          <header
            class="App-header"
          >
            <img
              alt="logo"
              class="App-logo"
              src="logo.svg"
            />
            <p>
              Edit
              <code>
                src/App.js
              </code>
               and save to reload.
            </p>
            <a
              class="App-link"
              href="https://reactjs.org"
              rel="noopener noreferrer"
              target="_blank"
            >
              Learn React
            </a>
          </header>
        </div>
      </div>
    </body>

      4 | test('renders learn react link', () => {
      5 |   render(<App />);
    > 6 |   const linkElement = screen.getByText(/learn react!!/i);
        |                              ^
      7 |   expect(linkElement).toBeInTheDocument();
      8 | });
      9 |

      at Object.getElementError (node_modules/@testing-library/dom/dist/config.js:38:19)
      at node_modules/@testing-library/dom/dist/query-helpers.js:90:38
      at node_modules/@testing-library/dom/dist/query-helpers.js:62:17
      at getByText (node_modules/@testing-library/dom/dist/query-helpers.js:111:19)
      at Object.<anonymous> (src/App.test.js:6:30)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        2.255 s
Ran all test suites.

Watch Usage
 › Press f to run only failed tests.
 › Press o to only run tests related to changed files.
 › Press q to quit watch mode.
 › Press p to filter by a filename regex pattern.
 › Press t to filter by a test name regex pattern.
 › Press Enter to trigger a test run.

再來就開始實作基本的範例吧


實作範例

這個範例很簡單,目的就是做一個容器顯示 count , 然後有兩個按鈕,一個是 +,一個是 -,點對應的按鈕就會對 count+-1 的操作。

因為範例很簡單,那這邊順便做一下 TDD(Test-driven development),先寫測試。

import { fireEvent, render, screen } from '@testing-library/react'
import React from 'react'
import App from './App'

test('renders + button', () => {
  render(<App />) // 先渲染 <App />
  const buttonElement = screen.getByRole('button', { name: '+' }) // 抓取 role 為 button, 且內容為 + 的按鈕
  expect(buttonElement).toBeInTheDocument() // 它存在畫面上
})

test('renders - button', () => {
  render(<App />)
  const buttonElement = screen.getByRole('button', { name: '-' })
  expect(buttonElement).toBeInTheDocument()
})

test('renders count span', () => {
  render(<App />)
  const spanElement = screen.getByTestId('count-span') // 從 testid 去抓 element
  expect(spanElement).toBeInTheDocument()
})

test('test + button function', () => {
  render(<App />)
  const buttonElement = screen.getByRole('button', { name: '+' })
  const spanElement = screen.getByTestId('count-span')
  fireEvent.click(buttonElement) // 對按鈕觸發點擊事件
  expect(spanElement.textContent).toBe('1') // 點擊後 span 的文字應該要為1
})

test('test - button function', () => {
  render(<App />)
  const buttonElement = screen.getByRole('button', { name: '-' })
  const spanElement = screen.getByTestId('count-span')
  fireEvent.click(buttonElement)
  expect(spanElement.textContent).toBe('-1')
})

這樣直接去跑測試

FAIL  src/App.test.js
  × renders + button (142 ms)
  × renders - button (34 ms)
  × renders count span (4 ms)
  × + button function (46 ms)
  × - button function (29 ms)

全部都噴掉很正常,畢竟你根本還沒開始寫,自然不會通過測試,那再來只要寫好讓測試都通過就完成了。

再來我們先把畫面做好,在 App.js 填入下面程式

import React from 'react'

function App() {
  return (
    <div className="App">
      <span data-testid="count-span">0</span>
      <button>+</button>
      <button>-</button>
    </div>
  )
}

export default App

再去跑一次測試

FAIL  src/App.test.js
  √ renders + button (70 ms)
  √ renders - button (16 ms)
  √ renders count span (4 ms)
  × + button function (16 ms)
  × - button function (12 ms)

會看到還是有錯誤,但是上面三個關於 render 的已經通過了,再來就缺少按鈕的功能

import React, { useState } from 'react'

function App() {
  const [count, setCount] = useState(0)

  return (
    <div className="App">
      <span data-testid="count-span">{count}</span>
      <button onClick={() => setCount(count + 1)}>+</button>
      <button onClick={() => setCount(count - 1)}>-</button>
    </div>
  )
}

export default App

再跑一次測試,會發現全部通過了

PASS  src/App.test.js
  √ renders + button (121 ms)
  √ renders - button (19 ms)
  √ renders count span (5 ms)
  √ + button function (20 ms)
  √ - button function (14 ms)

Test Suites: 1 passed, 1 total
Tests:       5 passed, 5 total
Snapshots:   0 total
Time:        2.697 s
Ran all test suites.

這時候去打開測試的 Server 會發現功能也正常。




最後更新時間: 2022年01月15日.