Vitest - 語法與常用 Matchers
基本語法
test / it
定義一個測試案例(test case)的最小單位,it 為 test 的別名,可替代使用。
import { expect, test } from "vitest";
test("should work as expected", () => {
  expect(Math.sqrt(4)).toBe(2);
});
describe
會形成一個作用域(scope),可以將同一個測試情境(test suite)的相關性測試案例(test case)集中起來。
import { describe, test, expect } from "vitest";
describe("Input 組件", () => {
  test("沒有輸入文字前,應該顯示 xxx placeholder",  () => {...});
  test("輸入文字後,搜尋按鈕要 enable",  () => {...});
  test("輸入字數超過最大字數限制時,應該顯示錯誤訊息",  () => {...});
});
vi
是 vitest 提供的輔助工具,可以提供模擬 Modules、函式、物件和時間等功能。
補充: vi 是對應 Jest 的 jest API。
import { expect, test, vi } from "vitest";
test("Element render correctly", async () => {
  // 等到 element 出現在頁面上,再對 element 做操作
  const element = await vi.waitUntil(() => document.querySelector(".element"), {
    timeout: 500, // default is 1000
    interval: 20 // default is 50
  });
  // do something with the element
  expect(element.querySelector(".element-child")).toBeTruthy();
});
斷言語法和常見 Matchers
斷言(Assertion) 是在測試中用來檢查程式碼是否按預期運行的語句,驗證某個條件是否為真或假(true/false)。根據測試比對的東西類型,所搭配使用的 Matchers 選擇也不同。
expect
用於創建斷言,程式碼執行結果與斷言設定一致的話,就會通過測試,支援 chai 和 Jest 斷言寫法。
expect(Math.sqrt(4)).to.equal(2); // chai API
expect(Math.sqrt(4)).toBe(2); // jest API
補充:
如果想檢查 type,可使用
expectTypeOf或assertType,或者使用expect+toBeTypeOf。expectTypeOf({ a: 1 }).toEqualTypeOf<{ a: number }>(); expectTypeOf({ a: 1 }).toEqualTypeOf({ a: 1 }); // 泛型檢查 function concat(a: string, b: string): string; function concat(a: number, b: number): number; function concat(a: string | number, b: string | number): string | number; assertType<string>(concat("a", "b")); assertType<number>(concat(1, 2)); // expect + toBeTypeOf test("stock is type of string", () => { expect("stock").toBeTypeOf("string"); });
純值
- 
toBe斷言值或斷言物件的 reference 是否相等。
補充:
- 
JavaScript 有浮點數精算問題,浮點數的計算要使用
toBeCloseTo - 
想檢查物件(Object)的結構是否相等,要使用
toEqual 
const stock = { type: "apples", count: 13 }; test("stock has 13 apples", () => { expect(stock.type).toBe("apples"); expect(stock.count).toBe(13); }); test("stocks are the same", () => { const refStock = stock; // same reference expect(stock).toBe(refStock); }); - 
 - 
toBeDefined/toBeUndefined斷言值是否為 undefined。
 - 
toBeNull、toBeNaN斷言值是否為 null / NaN。
 - 
toBeTruthy/toBeFalsy斷言值的 boolean 檢查。
補充:
null、undefined、NaN、0、-0、0n、""和document.all皆為 falsy 的種類。 - 
toMatch斷言字符串的字串匹配和正則表達式檢查。
test("top fruits", () => { expect("top fruits include apple, orange and grape").toMatch(/apple/); expect("applefruits").toMatch("fruit"); // toMatch also accepts a string }); - 
toBeGreaterThan/toBeGreaterThanOrEqual/toBeLessThan/toBeLessThanOrEqual斷言值與接收值的大小比較。
 
陣列
- 
toContain斷言值是否在陣列中。
test("the fruit list contains orange", () => { expect(getAllFruits()).toContain("orange"); }); - 
toContainEqual斷言物件是否在陣列中,物件結構和值必須完全相等(會遞迴比較)。
test("apple available", () => { expect(getFruitStock()).toContainEqual({ fruit: "apple", count: 5 }); }); - 
toMatchObject斷言物件是否有匹配的到另一個物件的部份屬性。
const johnInvoice = { customer: { first_name: "John", last_name: "Doe" }, items: [ { type: "apples", quantity: 10 }, { type: "oranges", quantity: 5 } ] }; const johnDetails = { customer: { first_name: "John", last_name: "Doe" } }; test("invoice has john personal details", () => { expect(johnInvoice).toMatchObject(johnDetails); });補充:
toMatchObject也可以接收陣列,用來檢查兩個陣列中的元素數量是否相等。test("the number of elements must match exactly", () => { // Assert that an array of object matches expect([{ foo: "bar" }, { baz: 1 }]).toMatchObject([ { foo: "bar" }, { baz: 1 } ]); }); - 
toHaveLength檢查字串、陣列的長度。
 
物件
- 
toEqual檢查斷言物件的值與接收到的物件的值是否相同結構(會遞迴比較)。
**補充:**如果物件結構裡面,欄位的值連出現 undefined 都要嚴格相等比較時,要選擇
toStrictEqual。// toEqual 與 toBe 比較 const stockBill = { type: "apples", count: 13 }; const stockMary = { type: "apples", count: 13 }; test("stocks have the same properties", () => { expect(stockBill).toEqual(stockMary); }); test("stocks are not the same", () => { expect(stockBill).not.toBe(stockMary); }); - 
toStrictEqual檢查斷言物件的值與接收到的物件的值是否相同結構和值(會遞迴比較)。
// toStrictEqual 與 toEqual 比較 test("structurally the same, but semantically different", () => { expect({ a: undefined, b: 2 }).toEqual({ b: 2 }); expect({ a: undefined, b: 2 }).not.toStrictEqual({ b: 2 }); }); - 
toHaveProperty斷言物件是否有包含有特定的 key,第二個可選參數可傳入該 key 預期的值(會遞迴比較)。
const invoice = { isActive: true, "P.O": "12345", customer: { first_name: "John", last_name: "Doe", location: "China" }, total_amount: 5000, items: [ { type: "apples", quantity: 10 }, { type: "oranges", quantity: 5 } ] }; test("John Doe Invoice", () => { // 純檢查 key 值是否存在 expect(invoice).toHaveProperty("isActive"); // 檢查 key 和 value expect(invoice).toHaveProperty("total_amount", 5000); // 使用 `.`,取得物件底下的 key 和該 value expect(invoice).toHaveProperty("customer.first_name"); expect(invoice).toHaveProperty("customer.last_name", "Doe"); expect(invoice).not.toHaveProperty("customer.location", "India"); // Array key 取法,可以用 array[index] 或 [keyPath] expect(invoice).toHaveProperty("items[0].type", "apples"); expect(invoice).toHaveProperty("items.0.type", "apples"); expect(invoice).toHaveProperty(["items", 0, "type"], "apples"); expect(invoice).toHaveProperty(["items", "0", "type"], "apples"); // string notation also works }); 
函式
- 
toHaveReturnedWith斷言函式至少被呼叫一次,並且成功返回帶有特定參數的值,需要將一個 spy 函式傳遞給 expect。
function add(a, b) { return a + b; } describe("add function", () => { it("should return the correct sum", () => { // 創建一個間諜函數 const spy = vi.fn(add); spy(1, 2); expect(spy).toHaveReturnedWith(3); }); }); - 
toHaveBeenCalled斷言函式函式至少被呼叫一次,需要將一個 spy 函式傳遞給 expect。
const market = { buy(subject: string, amount: number) { ... }, } test('spy function', () => { const buySpy = vi.spyOn(market, 'buy') expect(buySpy).not.toHaveBeenCalled() market.buy('apples', 10) expect(buySpy).toHaveBeenCalled() }) 
Error
- 
toThrowError斷言函式在被調用時是否會拋出錯誤,別名可用
toThrow,expect 裡面必須包裝成一個函式。function getFruitStock(type: string) { if (type === 'pineapples') { throw new Error('Pineapples are not in stock') } test('throws on pineapples', () => { // error msg 必須包含 stock 字串 expect(() => getFruitStock('pineapples')).toThrowError(/stock/) expect(() => getFruitStock('pineapples')).toThrowError('stock') // 利用正則表達式檢查 error msg 是否完全相等 expect(() => getFruitStock('pineapples')).toThrowError( /^Pineapples are not in stock$/ ) expect(() => getFruitStock('pineapples')).toThrowError( new Error('Pineapples are not in stock'), ) expect(() => getFruitStock('pineapples')).toThrowError(expect.objectContaining({ message: 'Pineapples are not in stock', })) })如果是非同步函式,需要搭配
rejects:function getAsyncFruitStock() { return Promise.reject(new Error("empty")); } // 非同步因為是回傳 promise,所以需要 async / await test("throws on pineapples", async () => { await expect(() => getAsyncFruitStock()).rejects.toThrowError("empty"); }); 
生命週期
下面這些 API 可以掛鉤到測試的生命週期,避免重複的前置準備和後續清理。
執行範圍會根據撰寫所在的範疇(context),如果是在 top-level 使用,則適用於整個文件;如果在 describe 區塊內使用,則適用於區塊內測試。
beforeAll(() => {
  // 在所有測試前建立一次性的測試環境
  db = initializeDatabase();
});
afterAll(() => {
  // 在所有測試後清理測試環境
  db.close();
});
describe("Database tests", () => {
  beforeEach(() => {
    // 在每個測試前重置數據庫
    db.reset();
  });
  afterEach(() => {
    // 在每個測試後清理數據庫
    db.cleanup();
  });
  it("should insert a record", () => {
    db.insert({ id: 1, name: "Test" });
    expect(db.find(1)).toEqual({ id: 1, name: "Test" });
  });
  it("should delete a record", () => {
    db.insert({ id: 1, name: "Test" });
    db.delete(1);
    expect(db.find(1)).toBeNull();
  });
});
beforeEach
在當前範疇(context)的每個測試執行前呼叫。beforeEach 還接受一個 optional 的清理函式(相當於 afterEach)。
適用情境:
在每個測試執行前需要進行一些重複的設置工作,例如:初始化數據庫連接、設置測試數據、重置變數等。
beforeEach(async () => {
  // 每個測試前都會執行
  await prepareSomething();
  // 每個測試後都會執行
  // clean up function
  return async () => {
    await resetSomething();
  };
});
afterEach
在當前範疇(context)的每個測試執行後呼叫。
適用情境:
在每個測試執行後需要進行一些清理工作,例如:清理數據庫、重置全局狀態、釋放資源等。
beforeAll
在當前範疇(context)的全部測試執行前呼叫。beforeAll 也還接受一個 optional 的清理函式(相當於 afterAll)。
適用情境:
在所有測試執行前需要進行一次性的設置工作,例如:建立一次性的測試環境、載入配置文件、初始化外部服務等。
afterAll
在當前範疇(context)的全部測試執行後呼叫。
適用情境:
在所有測試執行後需要進行一次性的清理工作,例如:釋放全局資源、關閉伺服器、清理測試環境等。