Learn React Testing Library Basics: Arrange, Act, and Assert

安裝設定

  • jsdom: 在 NodeJS 模擬瀏覽器 DOM
  • @testing-library/dom: 幫助我們在測試中檢視、操作模擬 DOM
  • @testing-library/react: @testing-library/dom 的其中一種 wrapper,提供基於 dom 之上關於 React 的方法和預先設定。

3 Main Parts of a Test

Arrange, Act, Assert

it("點擊名稱時,會顯示或隱藏 sub rows", () => {
// [Arrange]
// - render the component
// [Act 1]
// - 點擊名稱
// [Assert 1 after Act 1]
// - 確認對應的 sub-rows 出現在畫面中
// [Act 2]
// - 點擊名稱
// [Assert 2 after Act 2]
// - 確認對應的 sub-rows 消失在畫面中
});

Ref: Arrange-Act-Assert Pattern

Arrange

import { render } from "@testing-library/react";
it("點擊名稱時,會顯示或隱藏 sub rows", () => {
// [Arrange]
// - render the component
render(<Table {...testCaseProps} />);
});
  • 呼叫 @testing-library/reactrender,會在 jsdom 建立的 document.body 上 append 一個 <div>,再把參數的 React element render 到這個 <div>
  • RenderResult

Act

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
it("點擊名稱時,會顯示或隱藏 sub rows", () => {
// ...
// [Act]
// - 點擊名稱
userEvent.click(screen.getByText("麵類"));
// ...
});
  • 透過 @testing-library/domscreen 可以讀到上面說的 document.body,並且提供 API 讓我們可以指到裡面的特定元素
  • 透過 @testing-library/user-event,可以在 jsdom 中模擬使用者在瀏覽器的行為
    • user-event@14 要 React 18,目前我們是用 user-event@13,看官網文件和範例要小心版本。

Assert

it("點擊名稱時,會顯示或隱藏 sub rows", () => {
// ...
// [Assert]
// - 確認對應的 sub-rows 出現在畫面中
const resultRows = screen.getAllByRole("row");
expect(screen.getByText("白醬海鮮義大利麵")).toBeInTheDocument();
expect(screen.getByText("番茄肉醬義大利麵")).toBeInTheDocument();
expect(resultRows.length).toBe(6);
// ...
});
  • 同 Act,透過 @testing-library/domscreen 可以讀到上面說的 document.body,並且提供 API 讓我們可以指到裡面的特定元素
  • expect().toBeInTheDocument() 哪來的?:
    • setupTest.js:
      import "@testing-library/jest-dom";
    • @testing-library/jest-dom 會執行 Jest 的 expect.extend(extensions) 注入擴充方法

怎麼在 screen 上取得要的元素?

  • @testing-library/dom 的各種 queries API

queries分類:依搜尋條件

  • 怎麼選要用哪種方法?用跟使用者行為最像的,上面官網的排序就是照作者建議的順序(越上面越優先): https://testing-library.com/docs/queries/about/#priority

  • 條件都是給 TextMatch,只傳單純字串是 exact match

    // Matching a string:
    screen.getByText("Hello World"); // full string match
    screen.getByText("llo Worl", { exact: false }); // substring match
    screen.getByText("hello world", { exact: false }); // ignore case
    // Matching a regex:
    screen.getByText(/World/); // substring match
    screen.getByText(/world/i); // substring match, ignore case
    screen.getByText(/^hello world$/i); // full string match, ignore case
    screen.getByText(/Hello W?oRlD/i); // substring match, ignore case, searches for "hello world" or "hello orld"
    // Matching with a custom function:
    screen.getByText((content, element) => content.startsWith("Hello"));

queries分類:依行為 get, query, find

  • 所以舉例來說,ByRole 就會有 getByRole, queryByRole, findByRole, getAllByRole, queryAllByRole, findAllByRole

withIn

  • 找在特定元素底下的元素
  • Ref: withIn
import { render, screen, within } from "@testing-library/react";
render(<MyComponent />);
const messages = screen.getByText("messages");
const helloMessage = within(messages).getByText("hello");

更多關於 Arrange

可以檢查 render 的結果嗎?

  • const { debug } = render()screen.debug() 看 render 結果:
  • 會 log 出經過 prettyDOM 排版的 document.body
    <body>
    <div>
    <table
    class="BaseTable-ipSFEd ivBdWu with-nested-row"
    role="table"
    >
    <!-- ... -->
    </table>
    </div>
    <body>

自動清理

  • jsdom 會根據檔案產生 jsdom instance。換句話說不同檔案之間的模擬 DOM 是獨立的。(refA, refB)
    • 換句話說,在同一個 test file 裡面的不同 test 是共享同一個 jsdom instance,同樣的 DOM。
  • @testing-library/react 會自動在 afterEach 把 component umnount,不用自己 umnount. (ref)

更多關於 Act

user-eventfireEvent 差在哪?

  • fireEvent: 封裝後的 DOM event dispatcher,基本上還是一次呼叫對應一個 event
  • user-event: 設計成介面是使用者行為,一個行為可能觸發各種 event
  • 作者建議優先使用 user-event,但目前不一定所有使用者行為都有實作到

怎麼知道行為更新完畫面了?

  • 無論是 13 或 14 都已經透過 @testing-library/react 把各 libray API 設計成不需要在更新狀態時包 react-dom/test-utilsact

  • 如果還是出現 warning,通常是程式有非同步行為,造成的測試沒有寫好、沒等到行為完成。可參考:

    • wrapping updates with act is neither required nor recommended

    • Fix the not wrapped in act warning

    • Async Methods. Example:

      it("should show invalid field errors for each invalid input field", async () => {
      const { user } = renderApp();
      const userNameField = screen.getByPlaceholderText("Enter user name");
      const passwordField = screen.getByPlaceholderText("Enter password");
      await user.type(userNameField, "Philchard");
      await user.type(passwordField, "theCat");
      userEvent.click(
      screen.getByRole("button", {
      name: "Login",
      }),
      ); // need await if @14
      expect(await screen.findByText("Invalid User Name")).toBeVisible();
      expect(await screen.findByText("Invalid Password")).toBeVisible();
      });
  • user-event@13 方法幾乎都是同步的(不需要 await 行為完成,只有一個例外。但 user-event@14 都改成非同步了,如果升版會要 await。

更多關於 Assert

怎麼選擇 query 的方式?

  • 如前所述,作者建議選跟使用者行為最像的:https://testing-library.com/docs/queries/about/#priority
  • 為什麼作者最推薦用 role?作者的理想是網站實作較完整的 accecibilities。他想要機器讀到的網站結構,可以跟視覺 end user 讀到的視覺結構是一致的。這樣就可以假設測試模擬的行為跟視覺或聽覺使用者的行為都一致。
  • 不過因為實際上我們沒有做 accecibilities,所以網站並沒有完整的 aria- 等結構,只有特定 HTML element 本身的 role。在這樣的情況下,通常會用 text 來尋找。而沒有文字的就要用 test id 或 alt text 來尋找元素。

怎麼查 role?

不同時機的 query 結果是不同的

範例:resultRowsprevRows capture 到的是各自在呼叫的時間點符合條件的內容

userEvent.click(screen.getByText("麵類"));
// [Assert]
const resultRows = screen.getAllByRole("row");
expect(testCaseProps.onExpandedRowsChange).lastCalledWith(["1", "2"]);
expect(screen.getByText("白醬海鮮義大利麵")).toBeInTheDocument();
expect(screen.getByText("番茄肉醬義大利麵")).toBeInTheDocument();
expect(resultRows.length).toBe(6);
// [Act] 收合麵類 subrows
userEvent.click(screen.getByText("麵類"));
// [Assert]
const prevRows = screen.getAllByRole("row");
expect(testCaseProps.onExpandedRowsChange).lastCalledWith(["2"]);
expect(screen.queryByText("白醬海鮮義大利麵")).not.toBeInTheDocument();
expect(screen.queryByText("番茄肉醬義大利麵")).not.toBeInTheDocument();
expect(prevRows.length).toBe(4);