iT邦幫忙

2023 iThome 鐵人賽

DAY 8
0
Modern Web

React 測試 x AI:探索測試新境界,測試不再枯燥乏味!系列 第 8

[Day 08] React + Jest 表單驗證測試

  • 分享至 

  • xImage
  •  

從這篇開始我會使用 Vite + React + Jest + Testing Library 來做各個項目測試,而實作的部分就不會詳細說明只會大概講解流程,主要會著重在如何寫測試。那所有項目的程式碼都會放在 Practice Test,想要看各個項目可以切換 branch 來看。

今天要來測試的是表單驗證,這個功能在實際開發中是很常會用到的,我這邊是使用 formikyup 來進行表單管理驗證。

程式碼

先來看一下表單的程式碼:

export const FormPage = () => {
  const handleSubmit = (
    values: typeof initialValues,
    { resetForm }: FormikHelpers<typeof initialValues>
  ) => {
    alert(JSON.stringify(values));
    resetForm();
  };

  return (
    <div>
      <h2>Form</h2>
      <Formik
        initialValues={initialValues}
        validationSchema={Yup.object({
          firstName: requiredSchema().max(5, "不可大於 5 個字"), // 使用 yup 來驗證
          lastName: requiredSchema().max(10, "不可大於 10 個字"), // 使用 yup 來驗證
          twId: requiredSchema().concat(strAcctSchema()), // 使用 yup 來驗證
        })}
        onSubmit={handleSubmit}
      >
        <Form>
          <TextInput name='firstName' label='firstName' type='text' />
          <TextInput name='lastName' label='lastName' type='text' />
          <TextInput name='twId' label='taiwan id' type='text' />
          <button type='submit'>Submit</button>
        </Form>
      </Formik>
    </div>
  );
};

表單驗證的部分是使用 <Formik>validationSchema 來設定,這邊我設定了三個欄位,分別是 firstNamelastNametwId,其中 firstNamelastName 是必填,且不可超過 15、20 個字,twId 則是必填,且必須符合台灣身分證規則。

驗證規則則是另外拉函式出來,這樣可以讓程式碼更乾淨,也可以讓驗證規則可以重複使用。

以「必填」驗證為例:

import * as yup from "yup";

export default (msg = "必填項目") =>
  yup.string().test({
    name: "requiredSchema",
    exclusive: true, //同名以此為主
    params: { msg },
    message: `${msg}`,
    test: (value) => {
      return value !== "" && value !== undefined;
    },
  });

會 export 一個函式,這個函式可以接收一個參數,預設是「必填項目」,這樣就可以在驗證時傳入自訂的錯誤訊息。

Test Suites (測試套件)

基本上一個檔案會有一個對應的測試檔案,再根據檔案內的邏輯及功能來寫測試套件,這邊的測試套件 (Test Suites) 不是真的指一個套件,而是指很多的測試案例 (Test Cases) 的集合。

我這邊會把測試分成兩個部分,一個是表單邏輯(含 UI 顯示)的測試,另一個是表單的驗證測試。

  • 表單邏輯(含 UI 顯示)
    • 當未輸入任何欄位時,按下送出按鈕。
    • 當輸入欄位驗證失敗時,顯示錯誤訊息。
    • 當輸入欄位驗證通過時,按下送出按鈕。
  • 表單驗證
    • 必填項目驗證
    • 身分證驗證

表單邏輯(含 UI 顯示)

表單邏輯的測試,主要是測試當使用者在各個輸入情況下,按下送出按鈕,表單的行為是否符合預期,測試案例 (Test Case) 的描述會是這樣:

describe("Form page testing", () => {
  it("當未輸入任何欄位,按下送出按鈕,不會呼叫 alert 函式", () => {});
  it("當 firstName 欄位輸入「Alice」、lastName 欄位輸入「Robinson」、twId 欄位輸入「A123456789」,按下送出按鈕,呼叫 alert 函式", () => {});
  it("當 firstName 欄位輸入「Daniel」,顯示「不可大於 5 個字」", () => {});
  it("當 lastName 欄位輸入「Butterworth」,顯示「不可大於 10 個字」", () => {});
  it("當 twId 欄位輸入「A123」,顯示「身分證字號格式錯誤」", () => {});
});

這邊要注意的是,1 跟 2 是屬於單元測試,測試的是單一的邏輯,是否有執行某函式。而 3、4、5 則是屬於整合測試,測試的是使用者點擊,畫面有沒有顯示正確的錯誤訊息。

實際的測試程式碼如下:

window.alert = jest.fn(); // 模擬 alert 函式,也可以放在 setupTests.js 全域裡

describe("Form page testing", () => {
  const user = userEvent.setup(); // 設定 userEvent

  it("當未輸入任何欄位,按下送出按鈕,不會呼叫 alert 函式", async () => {
    render(<FormPage />); // 渲染表單頁面
    //記得 userEvent 是非同步的,所以要用 await
    await user.click(screen.getByRole("button", { name: /submit/i }));
    // 判斷 alert 函式有沒有被呼叫
    expect(alert).not.toHaveBeenCalled();
  });

  it("當 firstName 欄位輸入「Alice」、lastName 欄位輸入「Robinson」、twId 欄位輸入「A123456789」,按下送出按鈕,呼叫 alert 函式", async () => {
    render(<FormPage />);
    // 使用 user.type 來模擬使用者輸入
    await user.type(screen.getByLabelText(/firstname/i), "Alice");
    await user.type(screen.getByLabelText(/lastname/i), "Robinson");
    await user.type(screen.getByLabelText(/taiwan id/i), "A123456789");
    // 使用 user.click 來模擬使用者點擊
    await user.click(screen.getByRole("button", { name: /submit/i }));
    // 判斷 alert 函式有沒有被呼叫,且參數是否符合預期
    expect(alert).toHaveBeenCalledWith(
      JSON.stringify({
        firstName: "Alice",
        lastName: "Robinson",
        twId: "A123456789",
      })
    );
  });
  it("當 firstName 欄位輸入「Daniel」,按下送出按鈕,顯示「不可大於 5 個字」", async () => {
    render(<FormPage />);
    await user.type(screen.getByLabelText(/firstname/i), "Daniel");
    await user.click(screen.getByRole("button", { name: /submit/i }));
    expect(screen.getByText(/不可大於 5 個字/i)).toBeInTheDocument(); // 判斷畫面有沒有顯示錯誤訊息
  });
  it("當 lastName 欄位輸入「Butterworth」,按下送出按鈕,顯示「不可大於 10 個字」", async () => {
    render(<FormPage />);
    await user.type(screen.getByLabelText(/lastname/i), "Butterworth");
    await user.click(screen.getByRole("button", { name: /submit/i }));
    expect(screen.getByText(/不可大於 10 個字/i)).toBeInTheDocument();
  });
  it("當 twId 欄位輸入「A123」,按下送出按鈕,顯示「身分證字號格式錯誤」", async () => {
    render(<FormPage />);
    await user.type(screen.getByLabelText(/taiwan id/i), "A123");
    await user.click(screen.getByRole("button", { name: /submit/i }));
    expect(screen.getByText(/身分證字號格式錯誤/i)).toBeInTheDocument();
  });
});

表單驗證

表單驗證的測試,就是一般的函式測試,屬於單元測試,測試上就會相對單純,只要測試函式的回傳值是否符合預期就可以了。

import * as yup from "yup";

export default () =>
  yup.string().test({
    name: "twIdSchema",
    exclusive: true,
    params: {},
    message: "身分證字號格式錯誤",
    test: (value) => {
      if (value === undefined) return false;
      const letters = "ABCDEFGHJKLMNPQRSTUVXYWZIO";
      const firstLetterIndex = letters.indexOf(value[0]);
      if (firstLetterIndex < 0) {
        return false;
      }
      let sum =
        Math.floor((firstLetterIndex + 10) / 10) +
        ((firstLetterIndex + 10) % 10) * 9;
      for (let i = 1; i < 9; i++) {
        sum += +value[i] * (9 - i);
      }
      sum += +value[9] * 1;
      return sum % 10 === 0;
    },
  });
import * as yup from "yup";

export default (msg = "必填項目") =>
  yup.string().test({
    name: "requiredSchema",
    exclusive: true, //同名以此為主
    params: { msg },
    message: `${msg}`,
    test: (value) => {
      return value !== "" && value !== undefined;
    },
  });

測試程式碼:

describe("requiredSchema function testing", () => {
  it("輸入為「」,結果為 false", async () => {
    const schema = requiredSchema();
    const result = await schema.isValid("");
    expect(result).toBe(false);
  });
  it("輸入為「123」,結果為 true", async () => {
    const schema = requiredSchema();
    const result = await schema.isValid("123");
    expect(result).toBe(true);
  });
});

describe("strAcctSchema function testing", () => {
  it("輸入為「A123」,結果為 false", async () => {
    const schema = strAcctSchema();
    const result = await schema.isValid("A123");
    expect(result).toBe(false);
  });
  it("輸入為「A123456789」,結果為 true", async () => {
    const schema = strAcctSchema();
    const result = await schema.isValid("A123456789");
    expect(result).toBe(true);
  });
});

表單驗證算是比較常見的功能,測試的時候會把驗證跟 UI 顯示分開來測試,這樣比較好寫測試案例,也比較好維護。那今天的部分就到這邊~程式碼都在 Day-08,可以下載下來玩看看!


上一篇
[Day 07] 寫測試 AI 工具推薦
下一篇
[Day 09] React + Jest 表單驗證測試 (AI)
系列文
React 測試 x AI:探索測試新境界,測試不再枯燥乏味!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言