插件单元测试基础
插件测试与普通 Node 测试最大的区别:测试代码运行在 VS Code 的扩展宿主进程中,可以直接调用 vscode API。这意味着测试需要一套专门的脚手架,先下载一份独立的 VS Code,再用它加载插件并执行用例。yo code 生成项目时已经把这条链路铺好了,这篇文章逐文件拆解它。
脚手架:yo code 生成的测试目录
选择 "New Extension (TypeScript)" 模板生成项目后,与测试相关的文件如下:
my-extension/
├── .vscode/
│ ├── launch.json # 内置 "Extension Tests" 调试配置
│ └── tasks.json # 编译任务(preLaunchTask)
├── src/
│ ├── extension.ts # 被测插件主入口
│ └── test/
│ ├── runTest.ts # 主进程脚本:下载 VS Code 并启动测试
│ └── suite/
│ ├── index.ts # 测试运行器入口(加载 Mocha、收集用例)
│ └── extension.test.ts # 测试用例本体
├── package.json # scripts.test 指向 runTest.js
└── tsconfig.json| 文件 | 运行环境 | 职责 |
|---|---|---|
src/test/runTest.ts | Node 主进程(非 VS Code 内) | 调用 @vscode/test-electron 下载/定位 VS Code,以命令行参数启动并指向测试入口 |
src/test/suite/index.ts | 扩展宿主进程 | 创建 Mocha 实例,扫描并加载全部 .test.js 文件,执行后上报退出码 |
src/test/suite/extension.test.ts | 扩展宿主进程 | 编写具体的测试用例,可调用 vscode API |
理解这条链路的关键在"两段式":runTest.ts 跑在普通 Node 里,它只负责把 VS Code 拉起来;真正的测试在 VS Code 的扩展宿主里执行,只有那里才有 vscode 全局对象。
index.ts:测试运行器入口
src/test/suite/index.ts 是被 --extensionTestsPath 参数直接指定的入口,本质上是一个 Mocha 启动器:
import * as path from 'path';
import Mocha from 'mocha';
import { glob } from 'glob';
export async function run(): Promise<void> {
// 1. 创建 Mocha 实例:tdd 风格即 suite/test 语法,
// 与默认的 bdd 风格(describe/it)二选一
const mocha = new Mocha({
ui: 'tdd',
color: true
});
// 2. 测试根目录 = 本文件上一级(out/test/)
const testsRoot = path.resolve(__dirname, '..');
// 3. 扫描目录下所有 .test.js 文件(编译产物)
const files = await glob('**/**.test.js', { cwd: testsRoot });
// 4. 逐个加入 Mocha
files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f)));
try {
// 5. 运行并等待结果:失败数 > 0 时抛错
// 错误会沿调用链传回 runTest.ts,最终变成进程非零退出码
await new Promise<void>((resolve, reject) => {
mocha.run((failures) => {
if (failures > 0) {
reject(new Error(`${failures} tests failed.`));
} else {
resolve();
}
});
});
} catch (err) {
console.error(err);
throw err;
}
}glob 匹配的是编译后的 JS 文件——测试源码写 TypeScript,先经 npm run compile 产出到 out/,Mocha 加载的永远是 out/test/ 下的产物。所以改完测试代码必须先编译再运行,npm test 的 pretest 钩子会自动完成这一步。
测试运行器在扩展宿主中的执行机制
整个测试链路分三层,每一层的职责清晰:
npm test(package.json)
└─ node ./out/test/runTest.js ← 主进程,纯 Node 环境
└─ runTests()(@vscode/test-electron)
├─ downloadAndUnzipVSCode 下载独立测试版 VS Code
└─ 启动 code --extensionDevelopmentPath=插件目录
--extensionTestsPath=out/test/suite/index.js
└─ 扩展宿主进程加载插件 + index.js
└─ Mocha 扫描 *.test.js 并执行
└─ 用例内可直接 import vscode 调用 API关键点:
| 机制 | 说明 |
|---|---|
| 独立 VS Code | 测试下载的是全新的 VS Code 副本,与你日常使用的实例互不干扰,也不要求机器上装有 VS Code |
| 插件加载 | --extensionDevelopmentPath 指向插件目录,等同于 F5 调试时的加载方式 |
| 测试注入 | --extensionTestsPath 指定测试入口,VS Code 激活插件后立刻执行该文件 |
| 结果回传 | Mocha 失败数通过 Promise reject → runTest 捕获 → process.exit(1),CI 据此判断构建是否通过 |
第一个测试用例:extension.test.ts
模板自带的示例用例:
import * as assert from 'assert';
import * as vscode from 'vscode';
// tdd 风格:suite 组织分组,test 声明单个用例
suite('Extension Test Suite', () => {
// 测试运行前会先显示提示(可选)
vscode.window.showInformationMessage('开始运行全部测试');
test('示例用例:验证数组 indexOf 行为', () => {
assert.strictEqual(-1, [1, 2, 3].indexOf(5));
assert.strictEqual(-1, [1, 2, 3].indexOf(0));
});
test('示例用例:vscode 对象可用', () => {
// 这是插件测试与普通 Node 测试的本质区别:
// 可以直接断言全局 vscode API 的存在
assert.ok(vscode.workspace);
assert.ok(vscode.commands);
});
});用例内能访问 vscode 是因为测试运行在扩展宿主中。这也意味着可以断言插件激活后注册的命令:
import * as assert from 'assert';
import * as vscode from 'vscode';
suite('插件命令注册', () => {
test('helloWorld 命令存在且可执行', async () => {
// 查询命令注册表,确认激活时注册的命令
const commands = await vscode.commands.getCommands(true);
const found = commands.includes('myext.helloWorld');
assert.ok(found, 'myext.helloWorld 命令未注册');
});
});断言风格:assert 与 chai
脚手架默认使用 Node 内置的 assert,零依赖。需要更丰富的断言时可引入 chai:
import { expect } from 'chai';
suite('chai 断言示例', () => {
test('链式断言提升可读性', () => {
expect([1, 2, 3]).to.include(2);
expect({ name: 'vs' }).to.have.property('name').equal('vs');
expect('hello').to.be.a('string').with.lengthOf(5);
});
});| 断言库 | 风格 | 适用场景 |
|---|---|---|
node:assert | assert.strictEqual(实际, 期望) | 零依赖,脚手架默认 |
chai expect | expect(实际).to.equal(期望) | 链式可读,失败信息友好 |
chai assert | 与 node 内置同名 | 从 node 切换成本最低 |
引入 chai 需安装依赖:
{
"devDependencies": {
"chai": "^4.3.0",
"@types/chai": "^4.3.0"
}
}runTest.ts:下载并启动测试版 VS Code
src/test/runTest.ts 是 npm test 的入口,运行在主进程:
import * as path from 'path';
import { runTests } from '@vscode/test-electron';
async function main() {
try {
// 被测插件目录(package.json 所在处)
const extensionDevelopmentPath = path.resolve(__dirname, '../../');
// 测试运行器入口(编译后的 index.js,注意不带 .js 扩展名,
// runTests 内部会自动拼上)
const extensionTestsPath = path.resolve(__dirname, './suite/index');
// 下载(如未缓存)并启动 VS Code,自动执行测试
await runTests({
extensionDevelopmentPath,
extensionTestsPath
});
} catch (err) {
// 测试失败或启动失败都会走到这里
console.error('Failed to run tests:', err);
process.exit(1);
}
}
main();runTests 的常用选项:
| 参数 | 类型 | 说明 |
|---|---|---|
extensionDevelopmentPath | string 或 string[] | 被测插件目录,可同时传入多个 |
extensionTestsPath | string | 测试入口文件路径(无扩展名) |
version | string | 指定 VS Code 版本:'1.85.0'、'stable'、'insiders',默认最新 stable |
launchArgs | string[] | 附加启动参数,如 ['--disable-extensions']、['--user-data-dir=...'] |
extensionTestsEnv | 对象 | 注入到测试进程的环境变量,可用来区分测试模式 |
platform | string | 覆盖目标平台,如 'linux-x64',常用于 CI 交叉下载 |
@vscode/test-electron 的底层 API
runTests 内部是两步的组合,这两步也可单独使用,比如想在测试前用 CLI 安装另一个扩展:
import {
downloadAndUnzipVSCode,
resolveCliPathFromVSCodeExecutablePath
} from '@vscode/test-electron';
async function prepareVSCode() {
// 1. 下载指定版本 VS Code 到缓存目录,返回可执行文件路径
// 缓存位置:~/.vscode-test(Linux/macOS)或 %USERPROFILE%\.vscode-test(Windows)
const executablePath = await downloadAndUnzipVSCode('1.85.0');
// 2. 从可执行文件路径解析出 code CLI 路径
// CLI 可用于安装扩展、执行命令等(--install-extension xxx)
const cliPath = resolveCliPathFromVSCodeExecutablePath(executablePath);
console.log('VS Code 可执行文件:', executablePath);
console.log('CLI 路径:', cliPath);
return { executablePath, cliPath };
}两者与 runTests 的关系:
| API | 作用 |
|---|---|
downloadAndUnzipVSCode(version?) | 下载并解压测试版 VS Code,返回可执行文件路径;已缓存则直接返回 |
resolveCliPathFromVSCodeExecutablePath(path) | 由可执行文件路径推导 CLI 路径,用于 --install-extension 等场景 |
runTests(options) | 组合以上能力:定位 VS Code → 拼启动参数 → 拉起进程 → 等待测试结束 |
版本号支持 'stable'、'insiders'、具体版本号三种写法。CI 里通常固定版本号,保证每次构建行为一致。
异步测试与超时控制
插件测试大量涉及异步操作(打开文档、执行命令、等待事件),Mocha 默认 2 秒超时经常不够:
import * as assert from 'assert';
import * as vscode from 'vscode';
suite('异步与超时', () => {
test('打开文档并激活编辑器', async function () {
// 提升本用例超时到 10 秒:必须用 function 关键字,
// 箭头函数拿不到 this
this.timeout(10000);
// 在内存中创建未命名文档
const doc = await vscode.workspace.openTextDocument({ content: 'hello\nworld' });
// 显示到编辑器中,成为活动编辑器
await vscode.window.showTextDocument(doc);
// 断言活动编辑器指向刚打开的文档
const active = vscode.window.activeTextEditor;
assert.ok(active, '活动编辑器不应为空');
assert.strictEqual(active.document.uri.toString(), doc.uri.toString());
});
test('对超时用例使用 suite 级统一设置', async function () {
// 也可以把 this.timeout 提为整个 suite 的默认值
this.timeout(15000);
// ... 长耗时测试逻辑
});
});两个容易踩的坑:一是箭头函数没有自己的 this,调用 this.timeout 必须用 function;二是每个用例结束后记得清理资源(关闭文档、移除监听器),避免用例间相互污染。
运行 npm test
package.json 中测试相关的脚本:
{
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"pretest": "npm run compile && npm run lint",
"test": "node ./out/test/runTest.js"
}
}执行 npm test 时的动作顺序:pretest 先编译 TypeScript 并跑 lint → test 启动 runTest.js → 下载/复用测试版 VS Code → 加载插件执行用例 → 终端输出 Mocha 报告,失败则进程以非零码退出。
除了命令行方式,也可以直接在 VS Code 内调试测试:.vscode/launch.json 自带的 "Extension Tests" 配置把 --extensionTestsPath 指向测试入口,按 F5 即可在扩展宿主中单步调试测试代码,断点可以直接打在测试用例和被测插件的源码里:
{
"version": "0.2.0",
"configurations": [
{
"name": "Extension Tests",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
"--extensionTestsPath=${workspaceFolder}/out/test/suite/index"
],
"outFiles": ["${workspaceFolder}/out/test/**/*.js"],
"preLaunchTask": "${defaultBuildTask}"
}
]
}常见问题速查:
| 现象 | 原因与对策 |
|---|---|
报错 Cannot find module '@vscode/test-electron' | 忘记安装 devDependencies,执行 npm install |
| 测试代码修改后不生效 | 未重新编译,pretest 会编译,手动跑 node ./out/test/runTest.js 前先 npm run compile |
| 每次测试都很慢 | 首次下载 VS Code 较慢,之后命中缓存;CI 里可将 ~/.vscode-test 加入缓存 |
用例报 Timeout of 2000ms exceeded | 异步操作超时,用 this.timeout() 提高上限 |
脚手架就是这套流程的完整封装:runTest.ts 负责拉环境、index.ts 负责装测试、extension.test.ts 负责写断言,三层各司其职,npm test 一条命令贯通。掌握结构后,下一步就是把真实的插件逻辑装进这些用例里——这就是集成测试的内容。