テスト API
テスト API を使用すると、Visual Studio Code 拡張機能でワークスペース内のテストを検出し、結果を公開できるようになります。ユーザーは、[テスト エクスプローラー] ビュー、デコレーション、およびコマンド内からテストを実行できます。これらの新しい API により、Visual Studio Code は、従来よりもリッチな出力や差分の表示をサポートします。
注意: テスト API は、VS Code バージョン 1.59 以降で使用できます。
例
VS Code チームによって維持されている 2 つのテストプロバイダーがあります
- Markdown ファイルでテストを提供する サンプルテスト拡張機能。
- VS Code 自体でテストを実行するために使用している セルフホストテスト拡張機能。
テストの検出
テストは TestController によって提供されます。作成するには、グローバルに一意な ID と人間が読める形式のラベルが必要です
const controller = vscode.tests.createTestController(
'helloWorldTests',
'Hello World Tests'
);
テストを公開するには、コントローラーの items コレクションの子として TestItem を追加します。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) と同様に、テストがいつ検出されるかを制御するかどうかは、主に拡張機能に委ねられています。単純な拡張機能では、ワークスペース全体を監視し、アクティベーション時にすべてのファイルのすべてのテストを解析する場合があります。ただし、すべてを即座に解析すると、大きなワークスペースでは処理が遅くなる可能性があります。代わりに、次の 2 つのことができます
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 マップからアイテムを取得することが確実に機能します。
この例では、各アイテムの型を格納してみましょう
enum ItemType {
File,
TestCase
}
const testData = new WeakMap<vscode.TestItem, ItemType>();
const getType = (testItem: vscode.TestItem) => testData.get(testItem)!;
テストの実行
テストは TestRunProfile を介して実行されます。各プロファイルは、実行、デバッグ、またはカバレッジという特定の実行 kind に属します。ほとんどのテスト拡張機能では、これらのグループのそれぞれに最大 1 つのプロファイルがありますが、それ以上持つことも許可されています。たとえば、拡張機能が複数のプラットフォームでテストを実行する場合、プラットフォームと kind の組み合わせごとに 1 つのプロファイルを持つことができます。各プロファイルには 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 は、元のリクエストを渡して、少なくとも 1 回 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) を使用して汎用出力を追加できます。この出力は、Test: Show Output を使用してターミナルに表示したり、テスト エクスプローラー ビューのターミナル アイコンなどの UI 内のさまざまなボタンから表示したりできます。
文字列はターミナルでレンダリングされるため、ansi-styles npm パッケージで使用可能なスタイルを含め、ANSI コードの完全なセットを使用できます。ターミナル内であるため、一部のツールからのデフォルト出力である可能性のある単なる LF (\n) ではなく、CRLF (\r\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 のファイルカバレッジを複数回追加できます。その場合、新しい情報が古い情報を置き換えます。
ユーザーがカバレッジ付きのファイルを開くか、Test Coverage ビューでファイルを展開すると、VS Code はそのファイルに関する詳細情報を要求します。これを行うには、TestRun、FileCoverage、および CancellationToken を指定して、TestRunProfile 上の拡張機能定義の loadDetailedCoverage メソッドを呼び出します。テスト実行とファイルカバレッジのインスタンスは、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 は、DeclarationCoverage および/または StatementCoverage オブジェクトの配列へのプロミスを返すことが期待されています。どちらのオブジェクトにも、ソースファイル内でそれらが見つかる位置を示す Position または Range が含まれています。DeclarationCoverage オブジェクトには、宣言されているもの(関数名やメソッド名など)の名前と、その宣言が入力または呼び出された回数が含まれます。ステートメントには、実行された回数と、0 個以上の関連する分岐が含まれます。詳細については、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();
});
テスト エクスプローラー UI からの移行
テスト エクスプローラー UI を使用する既存の拡張機能がある場合は、追加機能と効率化のために、ネイティブのエクスペリエンスに移行することをお勧めします。Git 履歴にテストアダプターサンプルの移行例が含まれるリポジトリを用意しました。[1] Create a native TestController から始めて、コミット名を選択して各ステップを表示できます。
要約すると、一般的な手順は次のとおりです
-
テスト エクスプローラー UI の
TestHubにTestAdapterを取得して登録する代わりに、const controller = vscode.tests.createTestController(...)を呼び出します。 -
テストを発見または再発見したときに
testAdapter.testsを発火させるのではなく、代わりにテストを作成してcontroller.itemsにプッシュします。たとえば、vscode.test.createTestItemの呼び出しによって作成された発見されたテストの配列を指定してcontroller.items.replaceを呼び出します。テストが変更されるにつれて、テストアイテムのプロパティを変更したりその子を更新したりでき、変更は VS Code の UI に自動的に反映されることに注意してください。 -
最初からテストをロードするには、
testAdapter.load()メソッドの呼び出しを待つのではなく、controller.resolveHandler = () => { /* test discovery */ }を設定します。テスト検出の仕組みの詳細については、テストの検出を参照してください。 -
テストを実行するには、
const run = controller.createTestRun(request)を呼び出すハンドラー関数を持つ 実行プロファイルを作成する必要があります。testStatesイベントを発火させる代わりに、TestItemをrunのメソッドに渡して状態を更新します。
追加のコントリビューションポイント
testing/item/context メニュー コントリビューションポイントを使用して、テスト エクスプローラー ビューのテストにメニューアイテムを追加できます。メニューアイテムをインラインで表示するには、inline グループに配置します。その他のすべてのメニューアイテムグループは、マウスの右クリックを使用してアクセスできるコンテキストメニューに表示されます。
追加の コンテキストキー(testId、controllerId、testItemHasUri)が、メニューアイテムの when 句で使用できます。さまざまなテストアイテムに対してアクションをオプションで使用できるようにするような、より複雑な when シナリオでは、in 条件演算子の使用を検討してください。
エクスプローラーでテストを表示する場合は、テストをコマンド vscode.commands.executeCommand('vscode.revealTestInExplorer', testItem) に渡すことができます。