nikkie-ftnextの日記

イベントレポートや読書メモを発信

私は、VS Code拡張のE2E、書いてみたい! 〜今はこれが精一杯🔰、中間報告編〜

この記事は、Visual Studio Code Advent Calendar 2022 2日目の記事です。
VS Code拡張のテスト書いてみた」というエントリになります。
1日目はthe_redさんによる「VSCodeでSQLフォーマットするなら「Prettier SQL VSCode」で決まり!」でした。
便利そう!ですし、オススメされるVS Code拡張の実装は参考にできるところ多そうで気になりますね〜

はじめに

今はまだ勇気も自信もぜんぜんだから」、nikkieです。

Visual Studio Code、通称「VS Code」、「Prettier SQL VSCode」はじめ多様な拡張がありますよね。
そして拡張は自作もできます!
以前「歩夢」を「歩夢🎀」に置き換える(emojiを追加する)拡張を自作しました(どうかしてるぜ!)。

この拡張のE2Eを書こうと思い立ち、知ったこと・分かったことをこの記事にアウトプットします🔰

目次

「歩夢」を「歩夢🎀」に置き換える拡張

VS CodeMarkdownファイルを編集しているとき、「歩夢」という文字列があると「add oshi emoji」のマウスクリックでそこに 🎀 を追加できます!

VSCode Conference Japan 2021の「Hello VS Code Extension ハンズオンテキスト」を参考に実装し、手元で動かせました。

Markdownの中の「歩夢」という文字列にCodeLensを設定しています。
CodeLensとは別に、Rangeが渡されるとRangeに含まれる文字列に🎀を追加するコマンドも用意します。
CodeLensの設定では、「歩夢」という文字列のRangeを引数に、このコマンドを実行するように設定しています。

なので、CodeLensをクリックすると「歩夢」➡️「歩夢🎀」と置き換えられているわけです(埋め込んだツイートで様子が見られます)。

結論:E2Eで検証する項目の方針

CodeLensを使った拡張のE2Eは、以下を検証する方針で実装しました。
もっとよいやり方が浮かんだという方は、ぜひぜひ教えて下さい〜

  • CodeLensの設定の検証
    • Markdownファイルで「歩夢」を含む行にCodeLensが設定されているか
  • テスト中にコマンドを実行して結果を検証
    • 「歩夢」のRangeを渡してコマンドを実行Markdownファイルが書き換わるか

具体的な実装は該当するセクションをご覧ください。

きっかけ:なぜVS Code拡張のE2Eを書きたい?

きっかけは今年5月のPython tips LTラクスさん開催)。
Kawamataさんが自作のCopy Python Pathについて発表されていました。
記事バージョンはこちら。

「E2E テストで動作を担保」のところ、

実は VS Code 拡張機能のテストの方法は、ほぼほぼ情報がなくとても苦労しました。
VS Code のドキュメントにも Hello World レベルの簡単なテスト記載しかなく、結局、Microsoft の出している拡張機能の実際のテストコードをみながらテストを書きました。

苦労したと書かれていますが、情報がない中テストコード実装を完遂されていて、本当にすごい👏と舌を巻きました。

5月時点では年内にVS Code拡張を自作するとは思っていなかったのですが、ひょんなことから作ってみることに🎀。
作った後は「公開してみるか」と思ったのですが、個人の気持ち的に、「公開する(使ってもらえる可能性がある)以上はテストコードは用意したいなー」と。
そんなときにKawamataさんの発表を思い出し、書いてみた次第です。

なお、nikkieはPythonが大好きなのでPythonに極振りしており、TypeScriptは開発経験がありません🔰。
そしてJavaScriptについては、テスト周り(パッケージマネージャやテストフレームワーク)の知識が白紙状態、無限の伸びしろがあります。
VS Code拡張の開発も始めたばかりで、TypeScriptでの開発・JavaScriptのテスト・VS Code拡張の開発と新しいチャレンジが3つも重なり、「時間をかけても何も達成できないかもしれないな」という緊張感を久々に味わいました(暫定的な結論に至れて本当によかった🙌)。

テストコードの実行

テストコードの実行自体は簡単でした。
ハンズオンに沿ってYeomanでスキャフォールドしていて、以下のファイルができています。

.
├── src/
│   ├── extension.ts
│   └── test/
│       ├── runTest.ts
│       └── suite/
│           ├── index.ts
│           └── extension.test.ts
├── package.json
├── package-lock.json
└── tsconfig.json

この場合npm run testまたはyarn testでE2Eを実行できる1んだYO!
初回実行時にE2Eで操作されるVS CodeがダウンロードされたんだYO!

extension.test.tsに書かれているサンプルテストコードが通りました。

テストの2回めが実行できない事象に対処

macOSで開発しているのですが、テストをもう一度実行するとエラーに😱

これは以下のwarningが関係しているようで

WARNING: IPC handle "/.../.vscode-test/user-data/1.73.1-main.sock" is longer than 103 chars

私の環境では1.73.1-main.sockとおぼしきファイルが1.73.1-mとなっていました。

解決するには、src/test/runTest.tsrunTests関数呼び出しにlaunchArgsを追加して、--user-data-dir引数を渡します2
user-data-dirとして一時ディレクトリを渡しています。
warningには「try a shorter --user-data-dir」と続いており、これに沿った対応ですね!

import { tmpdir } from 'os';

async function main() {
    try {
        // ... 省略 ...
        await runTests(
            {
                extensionDevelopmentPath,
                extensionTestsPath,
                launchArgs: ['--user-data-dir', `${tmpdir()}`]
            });

出発点:こんなE2Eを書いてみたい

ケント・ベック氏の『テスト駆動開発』には 3A パターンが紹介されています(第19章)。

  • Arrange(準備)
  • Act(実行)
  • Assert(結果の検証)

これに則ると、最初に考えたE2Eは以下のようなものでした:

  • Arrange:テストに使うデータ(Markdownファイル)を用意し、VS Codeで開く
  • Act:CodeLens1つの「add oshi emoji」をクリック
  • Assert:「add oshi emoji」をクリックした行の文字列を検証(歩夢の後ろに🎀が挿入されたか)

この3つが実装できればE2Eがコードで書けたことになります。
Kawamataさんの記事や実装を確認しながら各Aをどう書くのか調べていきました。

E2E実装、はじめての旅路

Arrange:テストに使うデータの準備

Kawamataさんの実装を参考にすると、2箇所の設定が必要と分かりました。

.
├── src/
+ ├── test-fixtures/  # 追加!
+ │   └── markdown/
+ │       └── example.md
├── package.json
├── package-lock.json
└── tsconfig.json

src/test/runTest.tslaunchArgs引数にfixtureを置くディレクトリも渡します)

async function main() {
    try {
        const testWorkspace = path.resolve(__dirname, '../../test-fixtures');  // fixtureを置くディレクトリを指定

        await runTests(
            {
                extensionDevelopmentPath,
                extensionTestsPath,
                launchArgs: [testWorkspace, '--user-data-dir', `${tmpdir()}`]
            });

テストでは以下のようにしてVS CodeのウィンドウでMarkdownファイルを開いています。

const testFileLocation = '/markdown/example.md';

suite('Extension Test Suite', () => {
    let editor: vscode.TextEditor;

    test('Insert 🎀 after 歩夢', async () => {
        const fileUri = vscode.Uri.file(vscode.workspace.workspaceFolders![0].uri.fsPath + testFileLocation);
        const document = await vscode.workspace.openTextDocument(fileUri);
        editor = await vscode.window.showTextDocument(document);

        // Act と Assert はこれから書きます
    });
});

fixtureが読み込まれているか確認するためにテストを実行し、VS Codeが立ち上がりテスト用のMarkdownが表示されたのを見て一安心しました。

Assert:VS Codeで開いているファイルのある行の内容の検証

ハンズオンテキストに沿った実装でもファイルの中身を操作していたので、それを見返したり、APIドキュメントをたどったりしながら、行の内容を取得する方法を見つけました。

フィクスチャのMarkdown 5行目の文字列を取得して検証します(「add oshi emoji」実行前なので、🎀が挿入されていません)。

const testFileLocation = '/markdown/example.md';

suite('Extension Test Suite', () => {
    let editor: vscode.TextEditor;

    test('Insert 🎀 after 歩夢', async () => {
        const fileUri = vscode.Uri.file(vscode.workspace.workspaceFolders![0].uri.fsPath + testFileLocation);
        const document = await vscode.workspace.openTextDocument(fileUri);
        editor = await vscode.window.showTextDocument(document);

        // Act はこれから書きます

        const actual = editor.document.lineAt(4).text;
        assert.strictEqual(actual, '歩夢の行にemojiを追加できる');
    });
});

Act:もしかして、VS Codeを操作して「add oshi emoji」をクリックできない?

ArrangeとAssertが実装できたので、あとはAct!

const testFileLocation = '/markdown/example.md';

suite('Extension Test Suite', () => {
    let editor: vscode.TextEditor;

    test('Insert 🎀 after 歩夢', async () => {
        const fileUri = vscode.Uri.file(vscode.workspace.workspaceFolders![0].uri.fsPath + testFileLocation);
        const document = await vscode.workspace.openTextDocument(fileUri);
        editor = await vscode.window.showTextDocument(document);

        // 「add oshi emoji」をクリックする Act をここに書きたいよ!!

        const actual = editor.document.lineAt(4).text;
        assert.strictEqual(actual, '歩夢🎀の行にemojiを追加できる');
    });
});

editorを操作してCodeLensを実行できるのかな〜」とGoogle検索したりドキュメントを追ったりと探したのですが、その方法はわからず😢

代わりに以下の方法で検証することにしました。

CodeLensを実行する代わりにActはこう実装しました

CodeLensの設定の検証

Kawamataさんが参考にした拡張typescript-language-featuresにCodeLensのE2Eを見つけました。

https://github.com/microsoft/vscode/blob/1.73.1/extensions/typescript-language-features/src/test/smoke/referencesCodeLens.test.ts#L76-L80

const codeLenses = await getCodeLenses(testDocumentUri);
assert.strictEqual(codeLenses?.length, 3);
assert.strictEqual(codeLenses?.[0].range.start.line, 0);
assert.strictEqual(codeLenses?.[1].range.start.line, 1);
assert.strictEqual(codeLenses?.[2].range.start.line, 2);

このテストは

  • ActでVS CodeからすべてのCodeLensを取得し
  • AssertでCodeLensの個数や、CodeLensが対応する行との関係を検証し

ています。

これにならって、Markdownの「歩夢」がある行にCodeLensが設定されるかを検証することにしました。

const testFileLocation = '/markdown/example.md';

suite('Extension Test Suite', () => {
    test('Count CodeLenses', async () => {
        const fileUri = vscode.Uri.file(vscode.workspace.workspaceFolders![0].uri.fsPath + testFileLocation);
        const document = await vscode.workspace.openTextDocument(fileUri);
        editor = await vscode.window.showTextDocument(document);
        await sleep(500);  // Kawamataさんの実装にあるsleep(待機)です。Pythonのsleepのようにブロックすると理解しました

        const codeLenses = await vscode.commands.executeCommand<readonly vscode.CodeLens[]>('vscode.executeCodeLensProvider', fileUri, 100);
        
        assert.strictEqual(codeLenses?.length, 2);
        assert.strictEqual(codeLenses?.[0].range.start.line, 2);
        assert.strictEqual(codeLenses?.[1].range.start.line, 4);
    });
});

テスト中にコマンドを実行して結果を検証

KawamataさんのVS Code拡張はCodeLensではなくコマンドを提供する実装のため、E2Eでもコマンドを実行しています。
CodeLensもコマンドを実行するように設定しているため、CodeLensから実行される(はずの)コマンドを実行するテストケースも書きました。

「歩夢」という文字列のRangeを引数に、コマンドを実行し、その行の「歩夢」に🎀が追加されたかを検証します。
ユーザが「add oshi emoji」をクリックするのをシミュレートという意図です(E2Eに一番ふさわしいのは「add oshi emoji」の操作だと思うので、これを書くべきなのかあまり自信はありません…)。

const testFileLocation = '/markdown/example.md';

suite('Extension Test Suite', () => {
    let editor: vscode.TextEditor;

    test('Insert 🎀 after 歩夢', async () => {
        const fileUri = vscode.Uri.file(vscode.workspace.workspaceFolders![0].uri.fsPath + testFileLocation);
        const document = await vscode.workspace.openTextDocument(fileUri);
        editor = await vscode.window.showTextDocument(document);

        const COMMAND_NAME = "tokimeki-editing.addOshiEmoji";
        await sleep(1500);  // 'add oshi emoji'のロード待ち
        // 「歩夢」のRangeを渡してコマンド実行(コマンドは渡したRangeの文字列がなんであれ🎀を付ける)
        await vscode.commands.executeCommand(COMMAND_NAME, new vscode.Range(new vscode.Position(4, 0), new vscode.Position(4, 2)));
        await sleep(500);

        const actual = editor.document.lineAt(4).text;
        assert.strictEqual(actual, '歩夢🎀の行にemojiを追加できる');
    });
});

ブロックして待機するようにしたらテストが落ちる!?

CodeLensのロードを待とうとKawamataさんの実装にならってsleepを入れたのですが、そうするとテストが落ちるように😱

Error: Timeout of 2000ms exceeded.

これはMochaの設定で解決しました。

src/test/suite/index.ts

export function run(): Promise<void> {
    // Create the mocha test
    const mocha = new Mocha({
        ui: 'tdd',
        color: true,
        timeout: 5000  // 5秒に伸ばしました
    });

    // 省略(以下に変更はありません)

Kawamataさんの実装3typescript-language-features4でもMochaの設定がされているのですね。

終わりに

「自作のVS Code拡張にE2Eを書きたい!」と思い立ち、知っている拡張のテストコードからインプットしてテストコードを書いてみて、知ったことや分かったことをアウトプットしました。
TypeScriptでの開発・JavaScriptのテスト・VS Code拡張の開発と新しいチャレンジが3つ掛け合わさるという状況でしたが、暫定的な結論にはたどり着けました!

一番の収穫はE2Eのコードの悪戦苦闘を介して、自作のVS Code拡張の理解が進んだことです。
この拡張「歩夢歩夢」という行を「歩夢🎀歩夢🎀」といまはできませんが、どう変更すればいいのかアイデアが浮かびました。
また、他のemoji(👑、🎙など)の追加も全然いけそうです(「歩夢せつ菜」➡「歩夢🎀せつ菜🎙」とかもいけそう!)

実装はこちらにあります。あとは公開だっ!!

VS Code拡張のE2Eの実装やTypeScriptの書き方はまだぜんぜんだと思っているので、記事でお気づきの点があれば@ftnextまでお寄せいただけると本当に助かります(ブルーベリー本がこっちを見ているぞ)。
「CodeLensのE2Eを書きたいならこの拡張のテストコードが参考になるよ」といった"ここみるといいよ"情報も嬉しいです。

明日のVisual Studio Code Advent Calendar 2022 3日目はStreamWest-1629さんです!
「Devcontainerについて書く」そうです。楽しみですね〜!!

P.S. 本記事の秘密

開発のモチベーションは、ニジガク(ラブライブ!虹ヶ咲学園スクールアイドル同好会)からいただいています。

タイトルは1期1話の歩夢ちゃんのセリフからのオマージュでした。

MV置いておきますね(このMVの前後のセリフから着想しました)