测试 API
测试 API 允许 Visual Studio Code 扩展在工作区中发现测试并发布结果。用户可以在测试资源管理器视图中、从装饰器中以及在命令中执行测试。通过这些新的 API,Visual Studio Code 支持比以前更丰富的输出和差异显示。
注意:测试API在VS Code 1.59及更高版本中可用。
示例
VS Code团队维护了两个测试提供商:
发现测试
测试由 提供测试控制器,这需要一个全局唯一ID和可读标签来创建:
常量 控制器 = vscode.测试.创建测试控制器(
'helloWorldTests',
'Hello World Tests'
);
要发布测试,您需要添加测试项目s 作为儿童到控制器的项目收藏。测试项目s 是测试 API 的基础测试项目接口,并且是一个通用类型,可以描述在代码中存在的测试用例、测试套件或树项。它们可以,进而拥有儿童他们自己,形成一个层次结构。例如,这里是如何简化示例测试扩展创建测试的版本:
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);
}
// ...
});
类似于诊断,大多数情况下测试的发现由扩展控制。一个简单的扩展可能会在激活时监视整个工作区并解析所有文件中的所有测试。然而,对于大型工作区,立即解析所有内容可能会很慢。相反,你可以做两件事:
- 在编辑器中打开文件时,积极发现测试,通过监视
vscode.workspace.onDidOpenTextDocument输入:. - 设置
项目.可以解析子项 = true和设置控制器.解析处理程序。解决处理程序如果用户采取行动要求发现测试,例如通过在测试资源管理器中展开一项来执行测试,就会调用该方法。
这是一个在懒解析文件的扩展中可能看起来的策略:
// 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);
}
// 一些自定义逻辑,从内容中填充 test.children...
}
实施发现工作区中的所有文件可以使用 VS Code 的现有文件监控功能来构建。当解决处理程序被调用后,您应该继续监控以确保测试资源管理器中的数据保持最新。
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.
观察者.onDidDelete(uri => controller.items.delete(uri.toString()));
for (const file of await vscode.workspace.findFiles(pattern) {
getOrCreateFile(file);
}
返回 观察者;
})
);
}
该测试项目接口简单,没有足够的空间来存储自定义数据。如果你需要将额外的信息与一个测试项目,你可以使用一个弱映射输入:
常量 测试数据 = 新的 弱映射<vscode.测试项, 我的自定义数据>();
// 关联数据:
const item = controller.createTestItem(id, label);
testData.set(item, new MyCustomData());
// 后来再获取它:
常量 myData = 测试数据.获取(项目);
保证的是测试项目传递给所有实例测试控制器相关方法将与最初创建的方法相同创建测试项目, 所以您可以放心从测试数据地图将会工作。
在这个例子中,我们只需存储每个项目的类型:
枚举 项目类型 {
文件,
测试案例
}
常量 测试数据 = 新的 弱映射<vscode.测试项目, 项目类型>();
常量 获取类型 = (测试项目: vscode.测试项目) => 测试数据.获取(测试项目)!;
运行测试
测试通过执行测试运行配置文件每个配置文件属于特定的执行孩子运行、调试或覆盖。大多数测试扩展在这些组中最多只有一个配置文件,但允许更多。例如,如果你的扩展在多个平台上运行测试,你可以为每个平台组合有一个配置文件。孩子每个个人资料都有一个运行处理程序,当请求该类型运行时调用。
函数 runHandler(
shouldDebug: 布尔值,
request: vscode.TestRunRequest,
token: vscode.CancellationToken
) {
// 待办
}
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);
}
);
该运行处理程序应该打电话控制器创建测试运行至少一次,通过原始请求。请求包含测试包括在测试运行中(如果用户要求运行所有测试,则省略),并可能测试到排除从运行中。扩展程序应使用结果测试运行对象用于更新运行中涉及的测试的状态。例如:
异步 函数 运行处理程序(
应该调试: 布尔值,
请求: vscode.测试运行请求,
令牌: vscode.取消令牌
) {
const 运行 = 控制器.创建测试运行(请求);
const 队列: vscode.测试项[] = [];
// 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.消息), 日期.现在() - 开始);
}
break;
}
测试.儿童.forEach(测试 => 队列.推入(测试));
}
// 确保在所有测试执行完毕后结束运行:
run.end;
}
除了运行处理程序,你可以设置一个配置处理器在测试运行配置文件如果安装,VS Code 将会有用户界面允许用户配置测试运行,并在用户进行配置时调用处理程序。从这里,你可以打开文件、显示快速选择,或者执行适用于你的测试框架的任何操作。
VS Code 有意将测试配置与调试或任务配置处理得有所不同。这些传统上是围绕编辑器或 IDE 的功能,并且在特殊文件中进行配置。
.vscode然而,测试传统上是通过命令行执行的,大多数测试框架已经有现成的配置策略。因此,在 VS Code 中,我们避免配置的重复,而是让扩展来处理。
测试输出
除了传递给的消息测试运行失败或测试运行.错误,您可以使用 附加通用输出run.appendOutput(str)。此输出可以通过终端使用测试:显示输出命令或通过用户界面中的各种按钮来显示,例如测试资源管理器视图中的终端图标。
由于字符串在终端中渲染,您可以使用完整的ANSI代码,包括ansi-stylesnpm包中可用的样式。请注意,由于是在终端中,必须使用CRLF来换行(输入:\r\n),不仅是LF输入:), 这可能是某些工具的默认输出。
测试覆盖
测试覆盖与测试运行通过运行.添加覆盖()方法。通常应该由运行处理程序s 的个人资料测试运行配置类型.覆盖但是可以在任何测试运行期间调用它。添加覆盖方法接受一个文件覆盖对象,是该文件中覆盖数据的总结:
异步 函数 运行处理程序(
应该调试: 布尔值,
请求: vscode.测试运行请求,
令牌: vscode.取消令牌
) {
// ...
for await (const file of readCoverageOutput()) {
run.addCoverage(new vscode.FileCoverage(file.uri, file.statementCoverage));
}
}
该文件覆盖包含每个文件中语句、分支和声明的总体覆盖和未覆盖的数量。根据您的运行时和覆盖格式,您可能会看到语句覆盖被称为行覆盖,或者声明覆盖被称为函数或方法覆盖。您可以多次为单个URI添加文件覆盖,此时新信息将替换旧信息。
一旦用户打开一个带有覆盖信息的文件或在测试覆盖视图中展开一个文件,VS Code 会请求该文件的更多信息。它通过调用扩展定义的加载详细覆盖方法在测试运行配置文件与测试运行,文件覆盖,和一个取消令牌注意,测试运行和文件覆盖实例与在 中使用的相同。运行.添加覆盖,这有助于关联数据。例如,您可以创建一个地图文件覆盖将对象映射到您的数据:
const coverageData = new WeakMap<vscode.FileCoverage, MyCoverageDetails>();
配置文件.加载详细覆盖 = (测试运行, 文件覆盖, 令牌) => {
返回 覆盖数据.获取(文件覆盖).加载(令牌);
};
async function runHandler(
shouldDebug: boolean,
request: vscode.TestRunRequest,
token: vscode.CancellationToken
) {
// ...
```plaintext
for await (const file of readCoverageOutput()) {
const coverage = new vscode.FileCoverage(file.uri, file.statementCoverage);
coverageData.set(coverage, file);
run.addCoverage(coverage);
}
```
或者你可以继承文件覆盖包括该数据的实现:
class MyFileCoverage extends vscode.FileCoverage {
// ...
}
profile.loadDetailedCoverage = async (testRun, fileCoverage, token) => {
return fileCoverage instanceof MyFileCoverage ? await fileCoverage.load() : [];
};
异步 函数 运行处理程序(
应该调试: 布尔值,
请求: vscode.测试运行请求,
令牌: vscode.取消令牌
) {
// ...
for await (const file of readCoverageOutput()) {
// 'file' 是 MyFileCoverage:
run.addCoverage(file);
}
}
加载详细覆盖预计会返回一个数组的承诺声明覆盖和/或语句覆盖对象。两个对象都包括一个职位或范围在源文件中可以找到它们。声明覆盖对象包含声明的事物的名称(例如函数或方法名称)以及该声明被输入或调用的次数。语句包括它们被执行的次数,以及零个或多个相关分支。参见中的类型定义vscode.d.ts欲了解更多信息。
在许多情况下,你可能会从测试运行中留下持久化的文件。将此类覆盖输出放入系统的临时目录中是最佳实践(你可以通过 )。os.tmpdir()),但你也可以通过聆听 VS Code 的提示,它不再需要保留测试运行来急切地清理它们:
import { promises as fs } from 'fs';
async function runHandler(
shouldDebug: boolean,
request: vscode.TestRunRequest,
token: vscode.CancellationToken
) {
// ...
运行.onDidDispose(异步 () => {
等待 文件系统.删除(覆盖输出目录, { 递归: 真, 强制: 真 });
});
}
测试标签
有时测试只能在特定配置下运行,或者完全不运行。对于这些用例,您可以使用测试标签。测试运行配置文件s 可以选择关联一个标签,如果有的话,只有具有该标签的测试可以在该配置文件下运行。再次强调,如果无法针对特定测试运行、调试或收集覆盖率的合格配置文件,这些选项将不会在用户界面中显示。
// 创建一个新的标签,ID 为 "runnable"
const runnableTag = new TestTag('runnable');
// 将其分配到一个配置文件中。现在这个配置文件只能执行带有该标签的测试。
runProfile.tag = runnableTag;
// 为所有适用的测试添加"可运行"标签。
for (const test of getAllRunnableTests()) {
test.tags = [...test.tags, runnableTag];
}
用户还可以在测试浏览器中通过标签进行筛选。
只读控制器
运行配置文件的存在是可选的。控制器允许创建测试,调用创建测试运行外面的运行处理程序在运行中更新测试的状态而无需配置文件。这个常见的使用场景是控制器从外部源(例如CI或总结文件)加载其结果。
在这种情况下,这些控制器通常应该通过可选的名字参数到创建测试运行,和假为了持久参数。传递假这里指示 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.项目);
} 否则 {
运行.失败(结果.项目, 新的 vscode.测试消息(结果.消息));
}
}
运行.结束();
});
从测试资源管理器界面迁移
如果您已经有使用测试浏览器界面的扩展,请考虑迁移到原生体验,以获得更多的功能和更高的效率。我们整理了一个包含测试适配器示例在其中迁移到原生体验的示例迁移的仓库。您可以选择提交名称查看每一步,从 Git历史记录[1] 创建一个原生的TestController输入:.
总结起来,一般步骤是:
-
与其检索和注册
测试适配器使用测试浏览器用户界面测试中心,致电const controller = vscode.tests.createTestController(...)输入:. -
而不是开火
测试适配器.测试当你发现或重新发现测试时,不要创建并推送测试到控制器.项目例如,通过拨打控制器.项目.替换通过调用发现了一系列测试vscode测试.创建测试项注意,随着测试的变化,您可以更改测试项目的属性并更新其子项,VS Code 的用户界面将自动反映这些更改。 -
要初始加载测试,而不是等待一个
testAdapter.load()方法调用,设置controller.resolveHandler = () => { /* 发现测试 */ }。查看有关测试发现如何工作的更多信息,请参见 测试发现。 -
要运行测试,您应该创建一个运行配置,其中包含一个调用
const run = controller.createTestRun(request). 与其开枪测试状态事件,通过测试项目s 到方法上运行更新他们的状态。
额外贡献点
该测试/项目/上下文 菜单贡献点 可以用于在测试资源管理器视图中添加测试菜单项。将菜单项放置在内联将它们内联显示。所有其他菜单项组将在右键单击时通过鼠标访问上下文菜单显示。
附加 上下文键 可在 当菜单项的条款:测试编号,控制器ID,和测试项目有URI对于更复杂的当场景,您希望为不同的测试项目提供可选的动作,请考虑使用在 条件运算符.
如果你想在资源管理器中显示测试,可以将测试传递到命令vscode.commands.executeCommand('vscode.revealTestInExplorer', testItem)输入:.