測試 API
測試 API 允許 Visual Studio Code 擴充套件發現工作區中的測試併發布結果。使用者可以在“測試資源管理器”檢視、裝飾器和命令內部執行測試。藉助這些新 API,Visual Studio Code 可以比以往任何時候都更豐富地顯示輸出和差異。
注意:測試 API 在 VS Code 版本 1.59 及更高版本中可用。
示例
VS Code 團隊維護著兩個測試提供程式
發現測試
測試由 TestController 提供,需要一個全域性唯一 ID 和易於閱讀的標籤來建立。
const controller = vscode.tests.createTestController(
'helloWorldTests',
'Hello World Tests'
);
要釋出測試,您需要將 TestItem 新增為控制器 items 集合的子項。TestItem 是 TestItem 介面中測試 API 的基礎,它是一個通用型別,可以描述程式碼中存在的測試用例、套件或樹項。它們反過來可以擁有自己的 children,從而形成一個層次結構。例如,以下是示例測試擴充套件建立測試的簡化版本:
parseMarkdown(content, {
onTest: (range, numberA, mathOperator, numberB, expectedValue) => {
// If this is a top-level test, add it to its parent's children. If not,
// add it to the controller's top level items.
const collection = parent ? parent.children : controller.items;
// Create a new ID that's unique among the parent's children:
const id = [numberA, mathOperator, numberB, expectedValue].join(' ');
// Finally, create the test item:
const test = controller.createTestItem(id, data.getLabel(), item.uri);
test.range = range;
collection.add(test);
}
// ...
});
與 Diagnostics 類似,測試的發現主要由擴充套件控制。一個簡單的擴充套件可能會在啟用時監視整個工作區並解析所有檔案中的所有測試。但是,立即解析所有內容對於大型工作區來說可能會很慢。您可以採取以下兩種措施:
- 透過監視
vscode.workspace.onDidOpenTextDocument,在檔案編輯器中開啟時主動發現該檔案的測試。 - 設定
item.canResolveChildren = true並設定controller.resolveHandler。如果使用者採取了要求發現測試的操作(例如,透過在“測試資源管理器”中展開某個項),則會呼叫resolveHandler。
以下是在惰性解析檔案的擴充套件中,此策略可能是什麼樣的:
// First, create the `resolveHandler`. This may initially be called with
// "undefined" to ask for all tests in the workspace to be discovered, usually
// when the user opens the Test Explorer for the first time.
controller.resolveHandler = async test => {
if (!test) {
await discoverAllFilesInWorkspace();
} else {
await parseTestsInFileContents(test);
}
};
// When text documents are open, parse tests in them.
vscode.workspace.onDidOpenTextDocument(parseTestsInDocument);
// We could also listen to document changes to re-parse unsaved changes:
vscode.workspace.onDidChangeTextDocument(e => parseTestsInDocument(e.document));
// In this function, we'll get the file TestItem if we've already found it,
// otherwise we'll create it with `canResolveChildren = true` to indicate it
// can be passed to the `controller.resolveHandler` to gets its children.
function getOrCreateFile(uri: vscode.Uri) {
const existing = controller.items.get(uri.toString());
if (existing) {
return existing;
}
const file = controller.createTestItem(uri.toString(), uri.path.split('/').pop()!, uri);
file.canResolveChildren = true;
return file;
}
function parseTestsInDocument(e: vscode.TextDocument) {
if (e.uri.scheme === 'file' && e.uri.path.endsWith('.md')) {
parseTestsInFileContents(getOrCreateFile(e.uri), e.getText());
}
}
async function parseTestsInFileContents(file: vscode.TestItem, contents?: string) {
// If a document is open, VS Code already knows its contents. If this is being
// called from the resolveHandler when a document isn't open, we'll need to
// read them from disk ourselves.
if (contents === undefined) {
const rawContent = await vscode.workspace.fs.readFile(file.uri);
contents = new TextDecoder().decode(rawContent);
}
// some custom logic to fill in test.children from the contents...
}
discoverAllFilesInWorkspace 的實現可以使用 VS Code 現有的檔案監視功能來構建。當呼叫 resolveHandler 時,您應該繼續監視更改,以便“測試資源管理器”中的資料保持最新。
async function discoverAllFilesInWorkspace() {
if (!vscode.workspace.workspaceFolders) {
return []; // handle the case of no open folders
}
return Promise.all(
vscode.workspace.workspaceFolders.map(async workspaceFolder => {
const pattern = new vscode.RelativePattern(workspaceFolder, '**/*.md');
const watcher = vscode.workspace.createFileSystemWatcher(pattern);
// When files are created, make sure there's a corresponding "file" node in the tree
watcher.onDidCreate(uri => getOrCreateFile(uri));
// When files change, re-parse them. Note that you could optimize this so
// that you only re-parse children that have been resolved in the past.
watcher.onDidChange(uri => parseTestsInFileContents(getOrCreateFile(uri)));
// And, finally, delete TestItems for removed files. This is simple, since
// we use the URI as the TestItem's ID.
watcher.onDidDelete(uri => controller.items.delete(uri.toString()));
for (const file of await vscode.workspace.findFiles(pattern)) {
getOrCreateFile(file);
}
return watcher;
})
);
}
TestItem 介面很簡單,沒有自定義資料的空間。如果您需要將額外資訊與 TestItem 相關聯,可以使用 WeakMap。
const testData = new WeakMap<vscode.TestItem, MyCustomData>();
// to associate data:
const item = controller.createTestItem(id, label);
testData.set(item, new MyCustomData());
// to get it back later:
const myData = testData.get(item);
可以保證傳遞給所有 TestController 相關方法的 TestItem 例項與從 createTestItem 最初建立的例項是相同的,因此您可以確定從 testData map 中獲取的項是有效的。
在本示例中,我們只儲存每個項的型別。
enum ItemType {
File,
TestCase
}
const testData = new WeakMap<vscode.TestItem, ItemType>();
const getType = (testItem: vscode.TestItem) => testData.get(testItem)!;
執行測試
測試透過 TestRunProfile 執行。每個配置檔案都屬於一個特定的執行 kind:執行 (run)、除錯 (debug) 或覆蓋率 (coverage)。大多數測試擴充套件在這幾組中最多有一個配置檔案,但允許更多。例如,如果您的擴充套件在多個平臺上執行測試,您可以為每個平臺和 kind 的組合建立一個配置檔案。每個配置檔案都有一個 runHandler,在請求該型別的執行時會呼叫它。
function runHandler(
shouldDebug: boolean,
request: vscode.TestRunRequest,
token: vscode.CancellationToken
) {
// todo
}
const runProfile = controller.createRunProfile(
'Run',
vscode.TestRunProfileKind.Run,
(request, token) => {
runHandler(false, request, token);
}
);
const debugProfile = controller.createRunProfile(
'Debug',
vscode.TestRunProfileKind.Debug,
(request, token) => {
runHandler(true, request, token);
}
);
runHandler 至少應呼叫一次 controller.createTestRun,並傳遞原始請求。請求包含要 include 在測試執行中的測試(如果使用者請求執行所有測試,則省略),以及可能要 exclude 從執行中排除的測試。擴充套件應使用生成的 TestRun 物件來更新測試執行所涉及的測試的狀態。例如:
async function runHandler(
shouldDebug: boolean,
request: vscode.TestRunRequest,
token: vscode.CancellationToken
) {
const run = controller.createTestRun(request);
const queue: vscode.TestItem[] = [];
// Loop through all included tests, or all known tests, and add them to our queue
if (request.include) {
request.include.forEach(test => queue.push(test));
} else {
controller.items.forEach(test => queue.push(test));
}
// For every test that was queued, try to run it. Call run.passed() or run.failed().
// The `TestMessage` can contain extra information, like a failing location or
// a diff output. But here we'll just give it a textual message.
while (queue.length > 0 && !token.isCancellationRequested) {
const test = queue.pop()!;
// Skip tests the user asked to exclude
if (request.exclude?.includes(test)) {
continue;
}
switch (getType(test)) {
case ItemType.File:
// If we're running a file and don't know what it contains yet, parse it now
if (test.children.size === 0) {
await parseTestsInFileContents(test);
}
break;
case ItemType.TestCase:
// Otherwise, just run the test case. Note that we don't need to manually
// set the state of parent tests; they'll be set automatically.
const start = Date.now();
try {
await assertTestPasses(test);
run.passed(test, Date.now() - start);
} catch (e) {
run.failed(test, new vscode.TestMessage(e.message), Date.now() - start);
}
break;
}
test.children.forEach(test => queue.push(test));
}
// Make sure to end the run after all tests have been executed:
run.end();
}
除了 runHandler 之外,您還可以為 TestRunProfile 設定 configureHandler。如果存在,VS Code 將提供 UI 以允許使用者配置測試執行,並在使用者進行配置時呼叫該處理程式。在此,您可以開啟檔案、顯示快速選擇選單,或執行適合您的測試框架的任何操作。
VS Code 故意以不同於除錯或任務配置的方式處理測試配置。這些傳統上是編輯器或 IDE 的核心功能,並且在
.vscode資料夾中的特殊檔案中進行配置。然而,測試傳統上是從命令列執行的,並且大多數測試框架都有現有的配置策略。因此,在 VS Code 中,我們避免重複配置,而是將其留給擴充套件來處理。
測試輸出
除了傳遞給 TestRun.failed 或 TestRun.errored 的訊息之外,您還可以使用 run.appendOutput(str) 追加通用輸出。此輸出可以在終端中使用“測試: 顯示輸出”以及 UI 中的各種按鈕(如“測試資源管理器”檢視中的終端圖示)進行顯示。
由於字串將在終端中渲染,因此您可以使用完整的ANSI 程式碼集,包括ansi-styles npm 包中可用的樣式。請記住,由於它在終端中,行必須使用 CRLF (\r\n) 換行,而不是僅使用 LF (\n),後者可能是某些工具的預設輸出。
測試覆蓋率
測試覆蓋率透過 run.addCoverage() 方法與 TestRun 相關聯。通常,這應該由 TestRunProfileKind.Coverage 配置檔案的 runHandler 完成,但也可以在任何測試執行期間呼叫它。addCoverage 方法接受一個 FileCoverage 物件,該物件是該檔案中覆蓋率資料的摘要。
async function runHandler(
shouldDebug: boolean,
request: vscode.TestRunRequest,
token: vscode.CancellationToken
) {
// ...
for await (const file of readCoverageOutput()) {
run.addCoverage(new vscode.FileCoverage(file.uri, file.statementCoverage));
}
}
FileCoverage 包含每個檔案中語句、分支和宣告的總體覆蓋和未覆蓋計數。根據您的執行時和覆蓋率格式,您可能會看到語句覆蓋率稱為行覆蓋率,或宣告覆蓋率稱為函式或方法覆蓋率。您可以為單個 URI 新增多次檔案覆蓋率,在這種情況下,新資訊將替換舊資訊。
一旦使用者開啟一個帶有覆蓋率的檔案或在“測試覆蓋率”檢視中展開一個檔案,VS Code 就會請求該檔案的更多資訊。它透過在 TestRunProfile 上呼叫擴充套件定義的 loadDetailedCoverage 方法來完成此操作,並傳遞 TestRun、FileCoverage 和 CancellationToken。請注意,測試執行和檔案覆蓋率例項與 run.addCoverage 中使用的例項相同,這對於關聯資料很有用。例如,您可以建立 FileCoverage 物件到您自己的資料的對映。
const coverageData = new WeakMap<vscode.FileCoverage, MyCoverageDetails>();
profile.loadDetailedCoverage = (testRun, fileCoverage, token) => {
return coverageData.get(fileCoverage).load(token);
};
async function runHandler(
shouldDebug: boolean,
request: vscode.TestRunRequest,
token: vscode.CancellationToken
) {
// ...
for await (const file of readCoverageOutput()) {
const coverage = new vscode.FileCoverage(file.uri, file.statementCoverage);
coverageData.set(coverage, file);
run.addCoverage(coverage);
}
}
或者,您可能需要繼承 FileCoverage 並實現包含該資料。
class MyFileCoverage extends vscode.FileCoverage {
// ...
}
profile.loadDetailedCoverage = async (testRun, fileCoverage, token) => {
return fileCoverage instanceof MyFileCoverage ? await fileCoverage.load() : [];
};
async function runHandler(
shouldDebug: boolean,
request: vscode.TestRunRequest,
token: vscode.CancellationToken
) {
// ...
for await (const file of readCoverageOutput()) {
// 'file' is MyFileCoverage:
run.addCoverage(file);
}
}
loadDetailedCoverage 預計會返回一個 Promise,該 Promise 解析為 DeclarationCoverage 和/或 StatementCoverage 物件陣列。這兩個物件都包含它們在原始檔中可以找到的 Position 或 Range。DeclarationCoverage 物件包含被宣告內容的名稱(如函式或方法名稱)以及該宣告被輸入或呼叫的次數。語句包括它們被執行的次數,以及零個或多個關聯的分支。有關更多資訊,請參閱 vscode.d.ts 中的型別定義。
在許多情況下,您可能會在測試執行中留下持久化的檔案。最佳實踐是將此類覆蓋率輸出放在系統的臨時目錄中(可以透過 require('os').tmpdir() 獲取),但您也可以透過偵聽 VS Code 不再需要保留測試執行的提示來積極地清理它們。
import { promises as fs } from 'fs';
async function runHandler(
shouldDebug: boolean,
request: vscode.TestRunRequest,
token: vscode.CancellationToken
) {
// ...
run.onDidDispose(async () => {
await fs.rm(coverageOutputDirectory, { recursive: true, force: true });
});
}
測試標籤
有時測試只能在特定配置下執行,或者根本不能執行。對於這些用例,您可以使用測試標籤。TestRunProfile 可以選擇性地帶有一個關聯的標籤,如果它們有,則只有具有該標籤的測試才能在該配置檔案下執行。同樣,如果不存在可以執行、除錯或收集特定測試覆蓋率的合格配置檔案,則 UI 中將不會顯示這些選項。
// Create a new tag with an ID of "runnable"
const runnableTag = new TestTag('runnable');
// Assign it to a profile. Now this profile can only execute tests with that tag.
runProfile.tag = runnableTag;
// Add the "runnable" tag to all applicable tests.
for (const test of getAllRunnableTests()) {
test.tags = [...test.tags, runnableTag];
}
使用者也可以在“測試資源管理器”UI 中按標籤進行篩選。
僅釋出控制器
執行配置檔案的存在是可選的。控制器可以建立測試,在 runHandler 之外呼叫 createTestRun,並更新執行中測試的狀態,而無需配置檔案。這種情況的常見用例是載入其結果來自外部源(如 CI 或摘要檔案)的控制器。
在這種情況下,這些控制器通常應將可選的 name 引數傳遞給 createTestRun,並將 persist 引數傳遞為 false。將 false 傳遞給這裡會指示 VS Code 不要保留測試結果,就像對待編輯器中的執行一樣,因為這些結果可以從外部重新載入。
const controller = vscode.tests.createTestController(
'myCoverageFileTests',
'Coverage File Tests'
);
vscode.commands.registerCommand('myExtension.loadTestResultFile', async file => {
const info = await readFile(file);
// set the controller items to those read from the file:
controller.items.replace(readTestsFromInfo(info));
// create your own custom test run, then you can immediately set the state of
// items in the run and end it to publish results:
const run = controller.createTestRun(
new vscode.TestRunRequest(),
path.basename(file),
false
);
for (const result of info) {
if (result.passed) {
run.passed(result.item);
} else {
run.failed(result.item, new vscode.TestMessage(result.message));
}
}
run.end();
});
從 Test Explorer UI 遷移
如果您有一個使用 Test Explorer UI 的現有擴充套件,我們建議您遷移到原生體驗以獲得更多功能和效率。我們整理了一個包含 Test Adapter 示例遷移的儲存庫,其Git 歷史記錄。您可以透過選擇提交名稱(從 [1] Create a native TestController 開始)來檢視每個步驟。
總之,一般步驟是:
-
而不是檢索
TestAdapter並將其註冊到 Test Explorer UI 的TestHub,而是呼叫const controller = vscode.tests.createTestController(...)。 -
不要在發現或重新發現測試時觸發
testAdapter.tests,而是將測試建立並推送到controller.items中,例如透過呼叫controller.items.replace並傳入一個透過呼叫vscode.test.createTestItem建立的已發現測試陣列。請注意,隨著測試的變化,您可以修改測試項的屬性並更新其子項,更改將自動反映在 VS Code 的 UI 中。 -
要初步載入測試,而不是等待
testAdapter.load()方法呼叫,請設定controller.resolveHandler = () => { /* discover tests */ }。有關測試發現如何工作的更多資訊,請參閱發現測試。 -
要執行測試,您應該建立一個具有處理程式函式的執行配置檔案,該函式呼叫
const run = controller.createTestRun(request)。而不是觸發testStates事件,而是將TestItem傳遞給run上的方法來更新它們的狀態。
其他貢獻點
testing/item/context 選單貢獻點可用於向“測試資源管理器”檢視中的測試新增選單項。將選單項放置在 inline 組中即可使其內聯顯示。所有其他選單項組將顯示在可透過滑鼠右鍵單擊訪問的上下文選單中。
在選單項的 when 子句中,可以使用其他上下文鍵:testId、controllerId 和 testItemHasUri。對於更復雜的 when 場景,如果您希望操作對不同的 Test Item 可選可用,請考慮使用in 條件運算子。
如果您想在資源管理器中顯示一個測試,可以將該測試傳遞給命令 vscode.commands.executeCommand('vscode.revealTestInExplorer', testItem)。