使用 Node.js 的測試執行器
Node.js 有一個靈活且強大的內建測試執行器。本指南將向您展示如何設定和使用它。
example/
├ …
├ src/
├ app/…
└ sw/…
└ test/
├ globals/
├ …
├ IndexedDb.js
└ ServiceWorkerGlobalScope.js
├ setup.mjs
├ setup.units.mjs
└ setup.ui.mjs
注意:glob 模式需要 node v21+,並且 glob 模式本身必須用引號包裹(否則,您將得到與預期不同的行為,它可能看起來能工作,但實際上並非如此)。
有些東西是你總是需要的,所以把它們放在一個基礎設定檔案中,如下所示。這個檔案將被其他更特定的設定檔案匯入。
通用設定
import { } from 'node:module';
('some-typescript-loader');
// TypeScript is supported hereafter
// BUT other test/setup.*.mjs files still must be plain JavaScript!
然後為每個設定建立一個專用的 setup 檔案(確保基礎 setup.mjs 檔案在每個檔案中都被匯入)。隔離設定的原因有很多,但最明顯的原因是 YAGNI + 效能:您設定的很多東西可能是特定於環境的模擬/樁(mocks/stubs),這些設定可能非常昂貴,並且會減慢測試執行速度。當您不需要它們時,您希望避免這些成本(您為 CI 支付的實際金錢、等待測試完成的時間等)。
下面的每個示例都取自真實世界的專案;它們可能不適用於您的專案,但每個示例都展示了廣泛適用的通用概念。
動態生成測試用例
有時,您可能希望動態生成測試用例。例如,您想對一堆檔案進行相同的測試。這是可能的,儘管有些晦澀。您必須使用 test(不能使用 describe)+ testContext.test
簡單示例
import from 'node:assert/strict';
import { } from 'node:test';
import { } from '…';
const = [
{
: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.3',
: 'WIN',
},
// …
];
('Detect OS via user-agent', { : true }, => {
for (const { , } of ) {
.(, () => .((), ));
}
});
高階示例
import from 'node:assert/strict';
import { } from 'node:test';
import { } from './getWorkspacePJSONs.mjs';
const = ['node.js', 'sliced bread'];
('Check package.jsons', { : true }, async => {
const = await ();
for (const of ) {
// ⚠️ `t.test`, NOT `test`
.(`Ensure fields are properly set: ${.name}`, () => {
.(.keywords, );
});
}
});
注意:在 23.8.0 版本之前,設定有很大不同,因為
testContext.test不會自動等待。
ServiceWorker 測試
ServiceWorkerGlobalScope 包含非常特定的 API,這些 API 在其他環境中不存在,並且它的一些 API 看起來與其他 API 相似(例如 fetch),但具有增強的行為。您不希望這些影響到不相關的測試。
import { } from 'node:test';
import { } from './globals/ServiceWorkerGlobalScope.js';
import './setup.mjs'; // 💡
();
function () {
. = new ();
}
import from 'node:assert/strict';
import { , , } from 'node:test';
import { } from './onActivate.js';
('ServiceWorker::onActivate()', () => {
const = .;
const = .(async function () {});
const = .(async function () {});
class extends {
constructor(...) {
super('activate', ...);
}
}
before(() => {
. = {
: { , },
};
});
after(() => {
. = ;
});
('should claim all clients', async () => {
await (new ());
.(..(), 1);
.(..(), 1);
});
});
快照測試
這些測試由 Jest 推廣開來;現在,許多庫都實現了此功能,包括從 v22.3.0 開始的 Node.js。它有多種用例,例如驗證元件渲染輸出和 基礎設施即程式碼(Infrastructure as Code)配置。無論用例如何,其概念都是相同的。
需要透過 --experimental-test-snapshots 啟用該功能,但沒有特定的配置要求。但為了演示可選配置,您可能會在現有的測試配置檔案中新增類似以下內容。
預設情況下,node 生成的檔名與語法高亮檢測不相容:.js.snapshot。生成的檔案實際上是 CJS 檔案,所以更合適的檔名應以 .snapshot.cjs 結尾(或者像下面那樣更簡潔地寫成 .snap.cjs);這在 ESM 專案中也能更好地處理。
import { , , , } from 'node:path';
import { snapshot } from 'node:test';
snapshot.();
/**
* @param {string} testFilePath '/tmp/foo.test.js'
* @returns {string} '/tmp/foo.test.snap.cjs'
*/
function () {
const = ();
const = (, );
const = ();
return (, `${}.snap.cjs`);
}
下面的示例演示了使用 testing library 對 UI 元件進行快照測試;請注意訪問 assert.snapshot 的兩種不同方式)
import { , } from 'node:test';
import { } from '@testing-library/dom';
import { } from '@testing-library/react'; // Any framework (ex svelte)
import { } from './SomeComponent.jsx';
('<SomeComponent>', () => {
// For people preferring "fat-arrow" syntax, the following is probably better for consistency
('should render defaults when no props are provided', => {
const = (< />).container.firstChild;
..(());
});
('should consume `foo` when provided', function () {
const = (< ="bar" />).container.firstChild;
this.assert.snapshot(());
// `this` works only when `function` is used (not "fat arrow").
});
});
⚠️
assert.snapshot來自測試的上下文(t或this),而不是node:assert。這是必要的,因為測試上下文可以訪問node:assert無法訪問的作用域(每次使用assert.snapshot時,您都必須手動提供它,例如snapshot(this, value),這將相當繁瑣)。
單元測試
單元測試是最簡單的測試,通常不需要任何特殊設定。您的大多數測試可能都是單元測試,因此保持此設定的最小化非常重要,因為設定效能的微小下降會放大併產生連鎖反應。
import { } from 'node:module';
import './setup.mjs'; // 💡
('some-plaintext-loader');
// plain-text files like graphql can now be imported:
// import GET_ME from 'get-me.gql'; GET_ME = '
import from 'node:assert/strict';
import { , } from 'node:test';
import { } from './Cat.js';
import { } from './Fish.js';
import { } from './Plastic.js';
('Cat', () => {
('should eat fish', () => {
const = new ();
const = new ();
.(() => .eat());
});
('should NOT eat plastic', () => {
const = new ();
const = new ();
.(() => .eat());
});
});
使用者介面測試
UI 測試通常需要一個 DOM,可能還需要其他瀏覽器特定的 API(例如下面使用的 IndexedDb)。這些設定往往非常複雜且昂貴。
如果您使用的 API(如 IndexedDb)非常孤立,那麼像下面這樣的全域性模擬可能不是好的方式。相反,也許可以將這個 beforeEach 移動到將要訪問 IndexedDb 的特定測試中。請注意,如果訪問 IndexedDb(或其他任何東西)的模組本身被廣泛訪問,要麼模擬那個模組(這可能是更好的選擇),要麼確實將它留在這裡。
import { } from 'node:module';
// ⚠️ Ensure only 1 instance of JSDom is instantiated; multiples will lead to many 🤬
import from 'global-jsdom';
import './setup.units.mjs'; // 💡
import { } from './globals/IndexedDb.js';
('some-css-modules-loader');
(, {
: 'https://test.example.com', // ⚠️ Failing to specify this will likely lead to many 🤬
});
// Example of how to decorate a global.
// JSDOM's `history` does not handle navigation; the following handles most cases.
const = ...(.);
.. = function (, , ) {
(, , );
..();
};
beforeEach();
function () {
.indexedDb = new ();
}
您可以有兩種不同級別的 UI 測試:一種是類似單元測試的(其中外部和依賴項被模擬),另一種是更偏向端到端的(其中只有像 IndexedDb 這樣的外部項被模擬,而其餘的呼叫鏈都是真實的)。前者通常是更純粹的選擇,而後者通常會推遲到透過像 Playwright 或 Puppeteer 這樣的工具進行全端到端的自動化可用性測試。下面是前者的一個示例。
import { , , , } from 'node:test';
import { } from '@testing-library/dom';
import { } from '@testing-library/react'; // Any framework (ex svelte)
// ⚠️ Note that SomeOtherComponent is NOT a static import;
// this is necessary in order to facilitate mocking its own imports.
('<SomeOtherComponent>', () => {
let ;
let ;
(async () => {
// ⚠️ Sequence matters: the mock must be set up BEFORE its consumer is imported.
// Requires the `--experimental-test-module-mocks` be set.
= .('./calcSomeValue.js', {
: .(),
});
({ } = await import('./SomeOtherComponent.jsx'));
});
('when calcSomeValue fails', () => {
// This you would not want to handle with a snapshot because that would be brittle:
// When inconsequential updates are made to the error message,
// the snapshot test would erroneously fail
// (and the snapshot would need to be updated for no real value).
('should fail gracefully by displaying a pretty error', () => {
.mockImplementation(function () {
return null;
});
(< />);
const = .queryByText('unable');
assert.ok();
});
});
});