交易工具

自動化測試基本上只是程式碼,會在有問題時擲回或導致錯誤。大多數的程式庫或測試架構都提供各種基本功能,方便您撰寫測試。

如上一節所述,這些基本功能幾乎一律會包含定義獨立測試 (稱為「測試案例」) 及提供斷言的方法。斷言是一種結合檢查結果的方式,可在有問題時擲回錯誤,可以視為所有測試基元的基本原始版本。

本頁面說明這些基本功能的通則。您選擇的架構可能會有類似下方的內容,但並不是確切的參照。

例如:

import { fibonacci, catalan } from '../src/math.js';
import { assert, test, suite } from 'a-made-up-testing-library';

suite('math tests', () => {
  test('fibonacci function', () => {
    // check expected fibonacci numbers against our known actual values
    // with an explanation if the values don't match
    assert.equal(fibonacci(0), 0, 'Invalid 0th fibonacci result');
    assert.equal(fibonacci(13), 233, 'Invalid 13th fibonacci result');
  });
  test('relationship between sequences', () => {
    // catalan numbers are greater than fibonacci numbers (but not equal)
    assert.isAbove(catalan(4), fibonacci(4));
  });
  test('bugfix: check bug #4141', () => {
    assert.isFinite(fibonacci(0)); // fibonacci(0) was returning NaN
  })
});

本範例建立了一組稱為「數學測試」的套件測試,並定義三個獨立測試案例,分別執行多項斷言。這些測試案例通常可以個別處理或執行,例如使用測試執行器中的篩選器標記。

斷言輔助裝置做為基本功能

大多數的測試架構 (包括 Vitest) 都包含在 assert 物件上的斷言輔助程式,可讓您根據某些expectation快速檢查回傳值或其他狀態。這樣的期望通常為「已知良好」的值。在前例中,我們知道第 13 個 Fibonacci 編號應為 233,因此我們可以直接使用 assert.equal 進行確認。

您可能也預期值採用特定形式、大於其他值或含有其他屬性。本課程不會涵蓋所有可能的斷言輔助程式,但測試架構至少一律會提供以下基本檢查:

  • 「truthy」檢查 (通常稱為「確定」檢查) 可檢查條件是否為 true,且與您編寫 if 來檢查項目是否成功或正確。這通常會以 assert(...)assert.ok(...) 的形式提供,且接受單一值加上選用註解。

  • 等式檢查,例如在數學測試範例中,預期物件的傳回值或狀態等於已知的良好值。這些規則適用於原始等式 (例如數字和字串) 或參照等式 (這些物件為相同物件)。基本上,這只是帶有 ===== 比較的「超值」檢查。

    • JavaScript 會區分寬鬆 (==) 和嚴格 (===) 等式。多數測試程式庫分別提供 assert.equalassert.strictEqual 方法。
  • 深入的等式檢查:擴大等式檢查的範圍,包括檢查物件、陣列和其他較複雜資料類型的內容,以及用來掃遍物件以進行比較的內部邏輯。這些做法非常重要,因為 JavaScript 沒有內建比較兩個物件或陣列內容的方式。舉例來說,[1,2,3] == [1,2,3] 一律為 false。測試架構通常包含 deepEqualdeepStrictEqual 輔助程式。

比較兩個值 (而非只有「truthy」檢查) 的斷言輔助程式通常會採用兩個或三個引數:

  • 實際值,由受測試的程式碼產生,或描述要驗證的狀態。
  • 預期值,通常採用硬式編碼 (例如常值數字或字串)。
  • 選填的註解,說明預期或可能失敗的情況,會在這行失敗時隨附相關說明。

結合斷言以建構各種檢查也很常見,因為它很難單靠正確確認系統狀態。例如:

  test('JWT parse', () => {
    const json = decodeJwt('eyJieSI6InNhbXRob3Ii…');

    assert.ok(json.payload.admin, 'user should be admin');
    assert.deepEqual(json.payload.groups, ['role:Admin', 'role:Submitter']);
    assert.equal(json.header.alg, 'RS265')
    assert.isAbove(json.payload.exp, +new Date(), 'expiry must be in future')
  });

Vitest 會在內部使用 Chai 斷言程式庫提供其斷言輔助程式,建議您查看其參考資料,找出適合您的程式碼的斷言和輔助程式。

Fluent 和 BDD 斷言

部分開發人員偏好一種斷言樣式,可稱為行為導向開發 (BDD) 或流感式宣告。這些也稱為「預期」輔助程式,因為檢查期望的進入點是名為 expect() 的方法。

預期輔助程式的行為與宣告類似 assert.okassert.strictDeepEquals 等簡單的方法呼叫相同,但有些開發人員會覺得,這些輔助程式更容易讀取。BDD 斷言可能如下所示:

// A failure here would generate "Expect result to be an array that does include 42"
const result = await possibleMeaningsOfLife();
expect(result).to.be.an('array').that.does.include(42);

// or a simpler form
expect(result).toBe('array').toContainEqual(42);

// the same in assert might be
assert.typeOf(result, 'array', 'Expected the result to be an array');
assert.include(result, 42, 'Expected the result to include 42');

由於採用「方法鏈結」的技術,這些斷言樣式可以運作,其中 expect 傳回的物件可持續與後續方法呼叫鏈結在一起。呼叫的某些部分 (包括上述範例中的 to.bethat.does) 沒有任何函式,只是為了使呼叫更容易讀取,且在測試失敗時可能會產生自動註解。(值得注意的是,expect 通常不支援選用註解,因為鏈結應清楚描述失敗情形)。

許多測試架構都支援 Fluent/BDD 和一般斷言。Vitest 可例如匯出這兩種做法,並在 BDD 方面稍微簡化了自己的做法。另一方面,Jest 預設僅包含預期方法

對不同檔案進行分組測試

編寫測試時,我們通常會提供隱含的群組,而不是將所有測試放在同一個檔案中,而是在多個檔案之間撰寫測試。事實上,測試執行器通常只會知道檔案的用途,因為使用預先定義的篩選器或規則運算式 (例如 Vitest) 會包含專案中所有副檔名為「.test.jsx」或「.spec.ts」等有效副檔名的檔案。

元件測試通常會位於測試中元件的對等檔案內,如下列目錄結構所示:

目錄中的檔案清單,包括「UserList.tsx」和「UserList.test.tsx」。
元件檔案和相關測試檔案。

同樣地,單元測試通常會放在要測試的程式碼附近。端對端測試也可以各自放在自己的檔案中,而整合測試甚至可能會放在專屬的資料夾中。當複雜測試案例擴大,需要專屬的非測試支援檔案 (例如測試所需的支援資料庫) 時,這些結構會很有用。

在檔案中進行群組測試

如前範例一樣,常見的做法是將測試置於對 suite() 的呼叫中,而該呼叫將使用 test() 設定的測試分組。套件通常不會自行測試,但可透過呼叫傳遞的方法,將相關測試或目標分組來提供結構。如果是 test(),傳遞的方法會說明測試本身的動作。

和斷言一樣,Fluent/BDD 裡有相當標準的相等性來對測試分組。以下列舉一些常見範例:

// traditional/TDD
suite('math tests', () => {
  test('handle zero values', () => {
    assert.equal(fibonacci(0), 0);
  });
});

// Fluent/BDD
describe('math tests', () => {
  it('should handle zero values', () => {
    expect(fibonacci(0)).toBe(0);
  });
})

在大多數架構中,suitedescribe 的行為與 testit 相同,沒有使用 expectassert 編寫宣告有更大的差異。

其他工具在安排套件和測試的方式則不盡相同,例如,Node.js 的內建測試執行器支援對 test() 建立巢狀結構的呼叫,以隱含建立測試階層。不過,Vitest 只允許使用 suite() 的這種巢狀結構,而不會執行在另一個 test() 中定義的 test()

和斷言一樣,技術堆疊提供的確切分組方法組合並非很重要。本課程將介紹這些摘要,但您將需要瞭解如何運用這些資訊,如何套用到您選擇的工具。

生命週期方法

將測試分組的原因之一,是提供在每次測試或一組測試中執行的設定和拆解方法,即使在檔案的頂層中亦然。大多數架構都提供四種方法:

針對每個 `test()` 或 `it()` 再來一次
測試執行前 `beforeEach()` `beforeAll()`
測試執行後 `afterEach()` `afterAll()`

舉例來說,您可能會希望在每次測試前預先填入虛擬使用者資料庫,並在之後清除:

suite('user test', () => {
  beforeEach(() => {
    insertFakeUser('bob@example.com', 'hunter2');
  });
  afterEach(() => {
    clearAllUsers();
  });

  test('bob can login', async () => { … });
  test('alice can message bob', async () => { … });
});

這有助於簡化測試。您可以分享常見的設定和拆解程式碼,而不是在每個測試中複製程式碼。另外,如果設定和拆解程式碼本身擲回錯誤,可能表示不涉及測試本身失敗的結構問題。

通用建議

考量這些基元時,請牢記以下幾個訣竅。

基本功能可以做為指引

請記住,此處的工具及基本功能,以及後續幾頁中的工具與 Vitest、Jest、Mocha、Web Test Runner 或任何其他特定架構不會完全一致。雖然我們使用 Vitest 做為一般指南,但請務必將這些內容對應至您選擇的架構。

視需要混合及比對斷言

測試是可能擲回錯誤的程式碼。每個執行器都會提供可能為 test() 的原始版本,用於描述不同的測試案例。

但是,如果該執行器也提供 assert()expect() 和斷言輔助程式,請注意這部分是為了方便起見,可以視需要略過。您可以執行任何可能擲回錯誤的程式碼,包括其他斷言程式庫或良好的老式 if 陳述式。

IDE 設定可以做為生命儲存工具

確保 IDE (例如 VSCode) 能存取所選測試工具的自動完成功能和說明文件,可提高工作效率。舉例來說,Chai 斷言程式庫中的 assert 有超過 100 個方法,因此以內嵌方式顯示正確內容的說明文件是很方便的做法。

對於部分在全域命名空間中填入測試方法的測試架構而言,這一點尤為重要。這雖然有些微差異,但只要將測試程式庫自動加入全域命名空間,通常就不匯入測試程式庫就可能發生:

// some.test.js
test('using test as a global', () => { … });

即使系統自動受到支援,我們仍建議您匯入輔助程式,讓 IDE 能夠清楚地查詢這些方法。(您在建構 React 時可能會遇到這個問題,因為部分程式碼集具有神奇的 React 全域,但有些則否,且需要使用 React 將程式碼匯入所有檔案。)

// some.test.js
import { test } from 'vitest';
test('using test as an import', () => { … });