vscode插件开发之 - TestController

TesController概要介绍

TestController 组件是用于实现自定义测试框架和集成测试结果的。它允许开发者定义自己的测试运行器,以支持在VSCode中运行和展示测试。以下是一些使用 TestController 组件的主要场景:

自定义测试框架:如果你正在开发或使用一个非标准的测试框架,你可以使用 TestController 来集成这个框架的测试结果。

语言特定的测试:对于某些语言或框架,VSCode可能没有内置的测试支持。使用 TestController,你可以为这些语言或框架添加测试支持。

集成外部测试工具:如果你需要在VSCode中展示由外部测试工具生成的测试结果,TestController 可以用来映射这些结果到VSCode的测试UI。

测试结果可视化:通过 TestController,你可以控制测试结果如何在VSCode的测试面板中展示,包括测试的通过、失败、跳过等状态。

如何开发一款自定义的测试框架插件

要开发一款基于vscode的自定义测试框架非常简单,有三个步骤。以读取markdown中的代码,执行测试为例子来看看如何自定义测试框架插件

步骤一:解析文档中内容,并将结果添加到testcontroller的testItem对象

下面的代码中,对给定的文档内容通过正则表达式进行match,获取到markdown文件中的测试name,experssion,expected内容,并将parse出来的内容用于构建TestItem。下图图一假设是文本上的代码内容,下图图二是parse出来的Test对象内容。

TypeScript 复制代码
function loadTestsFromDocument(testController: vscode.TestController, document: vscode.TextDocument) {
        const tests = parseTests(document.getText());
        for (const test of tests) {
                const testItem = testController.createTestItem(test.name, test.expression + "=" + test.expected);
                testController.items.add(testItem);
        }
}

interface Test {
        name: string;
        expression: string;
        expected: number;
}

function parseTests(text: string): Test[] {
        const testRegex = /^(\d+ \+ \d+) = (\d+) \/\/ (.+)$/gm;
        const tests: Test[] = [];
        let match;

        while ((match = testRegex.exec(text)) !== null) {
                tests.push({
                        name: match[3],
                        expression: match[1],
                        expected: parseInt(match[2])
                });
        }

        return tests;
}

上述代码中,TestController.createTestItem 方法用于创建一个新的 TestItem,它代表一个测试用例。以下是 createTestItem 方法的一些关键参数和它们的说明:输入参数:

id (string): 测试用例的唯一标识符。这里是用Test.name作为id

label (string): 测试用例的显示名称,通常在UI中展示给用户。这里是组装成的label信息,后面要通过解析label信息来执行测试。testItem.label=test.expression + "=" + test.expected

uri (vscode.Uri): 表示测试用例所属文件的位置。通常使用当前编辑器的文档URI。

range (vscode.Range): 测试用例在文件中的位置范围。这有助于用户快速定位到测试用例的代码。

children (TestItem[], optional): 如果这个测试用例是一个容器,比如一个测试套件,你可以在这里提供子测试用例的数组。

tags (string[], optional): 一组标签,可以用来对测试用例进行分类或标记。

步骤二:自定义测试执行逻辑

下面定义了一个简单的runTest逻辑,通过解析testItem.label信息,判断expected和actual的值是否相等来判断测试执行结果。

TypeScript 复制代码
async function runTest(testItem: vscode.TestItem) {
        const expression = testItem.label.split('=')[0].trim();
        const expected = parseInt(testItem.label.split('=')[1].trim());
        const actual = eval(expression);
        const result = actual === expected;

        if (result) {
                testItem.busy = false;
                return `${testItem.label}: PASSED`;
        } else {
                testItem.busy = false;
                return `${testItem.label}: FAILED`;
        }
}

步骤三:注册命令执行测试

定义好前面的内容后,就可以注册命令,将testController.items的内容转换成数组,在逐个执行runTest方法,并把执行结果通过showInfomationMessage显示出来。

TypeScript 复制代码
        vscode.commands.registerCommand('markdownTestController.runTests', async () => {
                const tests = Array.from(testController.items)
                const results: string[] = [];
                for (const [, testItem] of tests) {
                        vscode.window.showInformationMessage(JSON.stringify(testItem));
                        const resultMessage = await runTest(testItem);
                        results.push(resultMessage);

                }
                if (results.length > 0) {
                        vscode.window.showInformationMessage(results.join('\n'));
                } else {
                        vscode.window.showInformationMessage('No tests executed.');
                }
        });
}

编写好脚本后,就可以执行了,在markdown文件中准备了一个数学计算,然后执行命令,可以看到message中显示执行结果,另外,为了调试,这里还显示testItem对象的值。

除了自定义测试执行逻辑,实际在开发vscode测试相关类插件时,还可以调用第三方已有的测试工具执行测试,代码的例子是调用jest执行测试的例子。

上面只定义了一个简单的runTest逻辑,在实际项目中,更多的是集成第三方测试执行插件,例如集成jest,在runTest方法里面只需通过"child_process.exec(`npx jest -t "${testItem.label}" --json`"来执行对应的测试即可。下面的使用vscode的testcontroller等组件,集成test来执行测试的例子。所有代码如下所示:

TypeScript 复制代码
import * as vscode from 'vscode';
import * as child_process from 'child_process';

export function activate(context: vscode.ExtensionContext) {
        const testController = vscode.tests.createTestController('jestTestController', 'Jest Tests');
        context.subscriptions.push(testController);
        context.subscriptions.push(vscode.commands.registerCommand('extension.runJestTests', async () => {
                await runAllTests(testController);
        }));

        async function runAllTests(testController: vscode.TestController) {
                const testItems: vscode.TestItem[] = [];
                testController.items.forEach(testItem => testItems.push(testItem));
                const request = new vscode.TestRunRequest(testItems);
                const run = testController.createTestRun(request);
                let allTestsPassed = true;
                const testResults: { [key: string]: boolean } = {};
                for (const test of testItems) {
                        run.started(test);
                        const result = await runJestTest(test);
                        testResults[test.id] = result;
                        if (result) {
                                run.passed(test);
                        } else {
                                run.failed(test, new vscode.TestMessage('Test failed'));
                                allTestsPassed = false;
                        }
                }
                run.end();
                if (allTestsPassed) {
                        vscode.window.showInformationMessage('All tests passed.');
                } else {
                        vscode.window.showInformationMessage('Some tests failed. Check test results for details.');
                }
        }

        context.subscriptions.push(vscode.workspace.onDidOpenTextDocument(doc => {
                if (doc.languageId === 'typescript' || doc.languageId === 'javascript') {
                        loadTestsFromDocument(testController, doc);
                }
        }));

        vscode.workspace.textDocuments.forEach(doc => {
                if (doc.languageId === 'typescript' || doc.languageId === 'javascript') {
                        loadTestsFromDocument(testController, doc);
                }
        });
}

function loadTestsFromDocument(testController: vscode.TestController, document: vscode.TextDocument) {
        const tests = parseTests(document.getText());
        for (const test of tests) {
                const testItem = testController.createTestItem(test.name, test.name, document.uri);
                testController.items.add(testItem);
        }
}

interface Test {
        name: string;
}

function parseTests(text: string): Test[] {
        const testRegex = /test\(['"](.+)['"]/g;
        const tests: Test[] = [];
        let match;

        while ((match = testRegex.exec(text)) !== null) {
                tests.push({
                        name: match[1]
                });
        }

        return tests;
}

async function runJestTest(testItem: vscode.TestItem): Promise<boolean> {
        return new Promise((resolve) => {
                const options = {
                        cwd: vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders[0].uri.fsPath : undefined,
                };
                child_process.exec(`npx jest -t "${testItem.label}" --json`, options, (err, stdout, stderr) => {
                        if (err) {
                                console.error(stderr);
                                vscode.window.showErrorMessage(`Test failed to run: ${stderr}`);
                                resolve(false);
                        } else {
                                try {
                                        const result = JSON.parse(stdout);
                                        vscode.window.showInformationMessage(`${testItem.label}: ${result.numFailedTests === 0 ? 'Passed' : 'Failed'}`);
                                        resolve(result.numFailedTests === 0);
                                } catch (parseError) {
                                        console.error(`Failed to parse test result: ${parseError}`);
                                        vscode.window.showErrorMessage(`Failed to parse test result: ${parseError}`);
                                        resolve(false);
                                }
                        }
                });
        });
}

export function deactivate() { }

为了验证上面的插件是否工作,需要再准备一个包含jest测试的项目,该项目包含一个简单sum函数,以及用jest框架测试add函数的测试脚本。

TypeScript 复制代码
import { add } from './adder';

test('first', () => {
        expect(add(1, 2)).toBe(4);
});

test('second', () => {
        expect(add(0, 0)).toBe(0);
});

运行插件,可以看到执行了两个测试,其中一个成功,一个失败,说明成功调用jest执行了测试,并获取到了测试结果。结果如下图所示:

以上就是vscode插件开发TestController组件的使用介绍,在实际项目,例如playwright-vscode插件就会使用这些组件,完成对ui测试的执行。后续的博客将从源码层面来解析playwright-vscode插件实现原理。

相关推荐
小灰灰__42 分钟前
IDEA加载通义灵码插件及使用指南
java·ide·intellij-idea
Mortal_hhh2 小时前
VScode的C/C++点击转到定义,不是跳转定义而是跳转声明怎么办?(内附详细做法)
ide·vscode·stm32·编辑器
无极程序员2 小时前
PHP常量
android·ide·android studio
小黄人软件5 小时前
android浏览器源码 可输入地址或关键词搜索 android studio 2024 可开发可改地址
android·ide·android studio
叶知安9 小时前
如何用pycharm连接sagemath?
ide·python·pycharm
电子云与长程纠缠11 小时前
UE5.3中通过编辑器工具创建大纲菜单文件夹
java·ue5·编辑器
会发光的猪。11 小时前
如何在vscode中安装git详细新手教程
前端·ide·git·vscode
lucky九年12 小时前
vscode翻译插件
ide·vscode·编辑器
帅得不敢出门12 小时前
Gradle命令编译Android Studio工程项目并签名
android·ide·android studio·gradlew