iT邦幫忙

2023 iThome 鐵人賽

DAY 5
0
Modern Web

React進階班,用typescript與jest製作自己的custom hooks庫系列 第 5

[Day 5] 用第二種測試方式寫useSessionStorage測試

  • 分享至 

  • xImage
  •  

首先,先講一下昨天的錯誤拉
昨天的錯誤碼找到拉~
改了一下useLocalStorage.ts的readInitValue
發現如果不是JSON.stringify()設置的會有問題
改了一下改成判斷能不能被JSON.parse解析

const readInitValue = useCallback(() => {
    try {
      const item = localStorage.getItem(key);
      if (!item) {
        return initialValue;
      }
      try {
        // 嘗試把item變成JSON解析
        return JSON.parse(item);
      } catch (e) {
        // 如果只是純字串則回傳
        return item;
      }
    } catch (err) {
      console.error("Error reading from localStorage:", err);
      return initialValue;
    }
  }, [key, initialValue]);

-------------------------------分隔線-------------------------------------

今天要用第二種方法來寫了
這個方法的重點是不在乎最後的結果(不在乎最後的儲存的store)
而是要去追蹤是否真的有正確call 這個setItem function
裡面參數與這個行為是不是正確的

// Step 1: 製作Mock sessionStorage 模擬 sessionStorage的行為
// `getItem`: 用于模拟从 sessionStorage 中读取数据的行为。
// `setItem`: 用于模拟向 sessionStorage 中写入数据的行为。
// `clear`: 用于模拟清空 sessionStorage 的行为。
const mockSessionStorage = {
  getItem: jest.fn(),
  setItem: jest.fn(),
  clear: jest.fn(),
};

jest.fn() 是 Jest 測試框架提供的一個方法,用於創建模擬(Mock)函數。模擬函數允許你追踪它們如何被調用、被調用多少次、什麼參數被傳遞之類的。

首先我們跟昨天不一樣的是
我們這個測試只關心 localStorage 方法(如 getItem、setItem 等)是否被正確地調用,以及它們是否被傳遞了正確的參數,這個就是使用 Jest Mock 函數的時機。這種方式可以讓我們很容易地追蹤函數的調用狀況和傳遞的參數

接下來跟昨天一樣,將mockSessionStorage替換真實的sessionStorage

Object.defineProperty(global, "sessionStorage", { value: mockSessionStorage });

開始寫測試拉

我們這次測試寫的跟昨天不同,我們寫一點其他還可以測試的東西,這樣比較有趣,比如發生error時或getItem取不到的時候~
1.測試一開始sessionStorage就有初始值
2.測試確保sessionStorage可以設置值進去
3.測試確保如果 sessionStorage 中的值不是一個 JSON 字串,hook 依然能夠正確地讀取它。
4.測試確保即使 sessionStorage 拋出一個錯誤(比如由於存儲已滿或其他原因),hook 還是能夠正確地設置初始值。
5.測試確保在 sessionStorage 為空(或者指定的 key 不存在)的情況下,hook 能正確地設置初始值。
6.測試確保如果你傳入一個函數來更新存儲的值,這個函數能正確地接收到舊值作為參數,並正確地更新新值。

describe("useSessionStorage custom hook", () => {
  // 昨天有提到afterEach在做什麼,忘記的記得看昨天的喔
  // 每次it測試完清除模擬函數的調用信息
  afterEach(() => {
    (sessionStorage.getItem as jest.Mock).mockClear();
    (sessionStorage.setItem as jest.Mock).mockClear();
  });

  // 測試一開始sessionStorage就有初始值
  it("gets initial value from sessionStorage", () => {
    (sessionStorage.getItem as jest.Mock).mockReturnValueOnce('"someValue"');
    const { result } = renderHook(() => useSessionStorage("key", "default"));

    expect(result.current[0]).toBe("someValue");
  });

  //測試確保可以設置值進去
  it("sets value to sessionStorage", () => {
    const { result } = renderHook(() => useSessionStorage("key", "default"));

    act(() => {
      result.current[1]("newValue"); // 調用 setValue 函數
    });

    expect(sessionStorage.setItem).toHaveBeenCalledWith("key", '"newValue"');
  });

  it("should handle non-JSON values gracefully", () => {
    mockSessionStorage.getItem.mockReturnValueOnce("non-json-value");
    const { result } = renderHook(() => useSessionStorage("key", "default"));
    expect(result.current[0]).toBe("non-json-value");
  });

  it("should handle errors thrown by sessionStorage gracefully", () => {
    mockSessionStorage.getItem.mockImplementationOnce(() => {
      throw new Error("Some error");
    });
    const { result } = renderHook(() => useSessionStorage("key", "default"));
    expect(result.current[0]).toBe("default");
  });

  it("should set initial value if sessionStorage is empty", () => {
    mockSessionStorage.getItem.mockReturnValueOnce(null);
    const { result } = renderHook(() => useSessionStorage("key", "default"));
    expect(result.current[0]).toBe("default");
  });

  it("should handle function updater correctly", () => {
    const { result } = renderHook(() => useSessionStorage("key", 0));

    act(() => {
      result.current[1]((prev) => prev + 1);
    });

    expect(mockSessionStorage.setItem).toHaveBeenCalledWith(
      "key",
      JSON.stringify(1)
    );
  });
  // 更多測試用例...
});

看完會不會噗撒撒,看不懂在幹嘛,其實我覺得jest有一個很好的地方,他的function英文其實就在解釋他的動作,

mockReturnValueOnce是指他會回傳一次你所傳的參數,如果你在call 一次則會回傳undefined,所以在我們useSession有呼叫getItem的地方,他第一次就會回傳'"someValue"'

(sessionStorage.getItem as jest.Mock).mockReturnValueOnce('"someValue"')

toHaveBeenCalledWith用於檢查一個模擬函數(Mock Function)是否有被帶有特定參數調用過
這裡就是我們期望sessionStorage.setItem("key", '"newValue"')有被呼叫

expect(sessionStorage.setItem).toHaveBeenCalledWith("key", '"newValue"')

最後結論吧~

如果你需要對你的custom hook(例如:localStorage) 方法的內部行為要進行更細致的控制,使用昨天那種自定義的模擬類可能會更有用。
如果你只是想簡單地驗證方法是否被調用,以及它們是如何被調用的,則使用 jest.fn() 應該就足夠了

忘記說第三個方法,就是用jest-localstorage-mock套件拉,我考慮了一下,決定不講解這個套件了,這種只針對單樣東西的就自己可以再研究看看,就不特別用一篇講解了

完整程式碼

import { renderHook, act } from "@testing-library/react";
import { useSessionStorage } from "../src"; // 請對應您的文件結構

// Step 1: 製作Mock sessionStorage 模擬 sessionStorage的行為
// `getItem`: 用于模拟从 sessionStorage 中读取数据的行为。
// `setItem`: 用于模拟向 sessionStorage 中写入数据的行为。
// `clear`: 用于模拟清空 sessionStorage 的行为。
const mockSessionStorage = {
  getItem: jest.fn(),
  setItem: jest.fn(),
  clear: jest.fn(),
};

Object.defineProperty(global, "sessionStorage", { value: mockSessionStorage });

describe("useSessionStorage custom hook", () => {
  // 清除模擬函數的調用信息
  afterEach(() => {
    (sessionStorage.getItem as jest.Mock).mockClear();
    (sessionStorage.setItem as jest.Mock).mockClear();
  });

  // 測試一開始sessionStorage就有初始值
  it("gets initial value from sessionStorage", () => {
    (sessionStorage.getItem as jest.Mock).mockReturnValueOnce('"someValue"');
    const { result } = renderHook(() => useSessionStorage("key", "default"));

    expect(result.current[0]).toBe("someValue");
  });

  //測試確保可以設置值進去
  it("sets value to sessionStorage", () => {
    const { result } = renderHook(() => useSessionStorage("key", "default"));

    act(() => {
      result.current[1]("newValue"); // 調用 setValue 函數
    });

    expect(sessionStorage.setItem).toHaveBeenCalledWith("key", '"newValue"');
  });

  //測試確保如果 sessionStorage 中的值不是一個 JSON 字串,hook 依然能夠正確地讀取它。
  it("should handle non-JSON values gracefully", () => {
    mockSessionStorage.getItem.mockReturnValueOnce("non-json-value");
    const { result } = renderHook(() => useSessionStorage("key", "default"));
    expect(result.current[0]).toBe("non-json-value");
  });

  // 測試確保即使 sessionStorage 拋出一個錯誤(比如由於存儲已滿或其他原因),hook 還是能夠正確地設置初始值
  it("should handle errors thrown by sessionStorage gracefully", () => {
    mockSessionStorage.getItem.mockImplementationOnce(() => {
      throw new Error("Some error");
    });
    const { result } = renderHook(() => useSessionStorage("key", "default"));
    expect(result.current[0]).toBe("default");
  });

  // 測試確保在 sessionStorage 為空(或者指定的 key 不存在)的情況下,hook 能正確地設置初始值
  it("should set initial value if sessionStorage is empty", () => {
    mockSessionStorage.getItem.mockReturnValueOnce(null);
    const { result } = renderHook(() => useSessionStorage("key", "default"));
    expect(result.current[0]).toBe("default");
  });

  // 測試確保如果你傳入一個函數來更新存儲的值,這個函數能正確地接收到舊值作為參數,並正確地更新新值
  it("should handle function updater correctly", () => {
    const { result } = renderHook(() => useSessionStorage("key", 0));

    act(() => {
      result.current[1]((prev) => prev + 1);
    });

    expect(mockSessionStorage.setItem).toHaveBeenCalledWith(
      "key",
      JSON.stringify(1)
    );
  });
  // 更多測試用例...
});

上一篇
[Day 4] 開始寫useLocalStorage測試吧
下一篇
[Day 6] 再來寫最常使用到的useFetch吧
系列文
React進階班,用typescript與jest製作自己的custom hooks庫30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言