vscode拡張機能を作るときにテストコードを作成してテストする

# vscode拡張機能を作るときにテストコードを作成してテストする

拡張機能を公開する場合、機能追加やバグ修正に伴ってリグレッションテストなどが必要になると思います。
vscode拡張機能開発にはテストコードを用いたテスト機能が備わっています。
この記事では、vscode拡張機能開発時のテスト方法について解説します。

# 環境作成

typescriptを用いて開発を行っている場合、すでにtestフォルダが作成されていると思います。

![img1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/728667/8460bc68-8bc9-e5ae-b8ee-881da87e055a.png)


おそらくディレクトリ構成はこんな感じです。
├─src
│  │  extension.ts
│  │
│  └─test
│      │  runTest.ts
│      │
│      └─suite
│              extension.test.ts
│              index.ts


それぞれ内容は以下。

## runTest.ts

単体テストのエントリポイント。
ここから後述のindex.tsを呼び出している。

## suite/index.ts

このファイルから、extension.test.ts(コンパイル後にはjs)を呼び出している。

## suite/extension.test.ts

サンプル用のテスト。
配列[1,2,3]に5と0が見つからないことをテストしている。

# テストコードの書き方
extension.test.tsの形をコピペで良いです。
suite()関数にテストタイトルを入れ、関数の呼び出しをしたりvscodeAPIを用いて
値が正しいことの確認をすれば良いです。
ウィンドウが生成されてるので統合テスト的な使い方もできるかもしれないが未確認です。

注意点は以下です。
多分テストコード全般に共通することですが私自身が躓いたので書いておきます。

* vscode APIで定義しているinterfaceを引数に保つ場合、そのinterfaceを継承したクラスを作成し、テスト用のオブジェクトを作成する
  * クラス内で定義されているオブジェクトなどは、必要なモノ以外限定代入アサーション(!記号)を使います
* mochaと呼ばれるテスト用フレームワークを使っています。何かあればmochaについてググるといいかも
*  assert.strictEqualで戻り値と期待値が等しいことを確認。

以下に私の書いた、特定の語句がある行をアウトラインに出力するコードとそのテストコードを記載します。

```typescript:OutlineProvider.ts
import * as vscode from 'vscode';

export class OutlineProvider implements vscode.DocumentSymbolProvider{
	regExp:RegExp
	MATCH_TEXTS:Array<string>
	constructor() {
		this.regExp = /((\w+))\s*((\S*)=\"?(\w*)\"?)*()/;
		this.MATCH_TEXTS = ["if","elsif","else", "endif","ignore","endignore","jump", "call","button","link","s", "iscript", "endscript", "loadjs"];
	}
	public provideDocumentSymbols(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.ProviderResult<vscode.DocumentSymbol[] | vscode.SymbolInformation[]>{
		let symbols = [];
		for (let i = 0; i < document.lineCount; i++) {
			let line = document.lineAt(i);//i行目のドキュメントを取得
			let match = line.text.match(this.regExp);//[hoge param=""]の形式のタグでマッチしてるかを探して変数に格納
			if(!match){
				// return Promise.reject("unmatched."); //指定文字がなかった時。引数で与えられた理由でPromiseオブジェクトを返却
				continue;
			}
			let matchText = match[1];

			//matchTextがMATCH_TEXTSで定義したいずれかのタグがあるならアウトラインに表示
			for(let j = 0; j < this.MATCH_TEXTS.length; j++){
				if(matchText === this.MATCH_TEXTS[j]){
					let symbol = new vscode.DocumentSymbol(line.text, 'Component', vscode.SymbolKind.Class, line.range, line.range);
					symbols.push(symbol);
				}
			}

			//ラベルをアウトラインに表示
			if(line.text.startsWith("*")){
				let symbol = new vscode.DocumentSymbol(line.text, 'Component', vscode.SymbolKind.Function, line.range, line.range);
				symbols.push(symbol);
			}
		}
		return symbols;

	}
}


```

```typescript:hoge.test.ts
import * as assert from 'assert';

// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
import * as vscode from 'vscode';
import { OutlineProvider } from '../../OutlineProvider';
// import * as myExtension from '../../extension';

class MockTextDocument implements vscode.TextDocument{
	mockTextLine = new MockTextLine();
	uri!: vscode.Uri; //!で限定代入アサーション null,undefinedでないことをアサート
	fileName!: string;
	isUntitled!: boolean;
	languageId!: string;
	version!: number;
	isDirty!: boolean;
	isClosed!: boolean;
	save(): Thenable<boolean> {
		throw new Error('Method not implemented.');
	}
	eol!: vscode.EndOfLine;
	lineCount = 1;
	lineAt(line: number): vscode.TextLine;
	lineAt(position: vscode.Position): vscode.TextLine;
	lineAt(position: any): vscode.TextLine {
		return this.mockTextLine;
	}
	offsetAt(position: vscode.Position): number {
		throw new Error('Method not implemented.');
	}
	positionAt(offset: number): vscode.Position {
		throw new Error('Method not implemented.');
	}
	getText(range?: vscode.Range): string {
		return "ここに文字列";
		// throw new Error('Method not implemented.');
	}
	getWordRangeAtPosition(position: vscode.Position, regex?: RegExp): vscode.Range | undefined {
		throw new Error('Method not implemented.');
	}
	validateRange(range: vscode.Range): vscode.Range {
		throw new Error('Method not implemented.');
	}
	validatePosition(position: vscode.Position): vscode.Position {
		throw new Error('Method not implemented.');
	}
}

class MockCancellationToken implements vscode.CancellationToken{
	isCancellationRequested!: boolean;
	onCancellationRequested!: vscode.Event<any>;
	
}

class MockTextLine implements vscode.TextLine{
	lineNumber!: number;
	text="1";
	range = new vscode.Range(new vscode.Position(0,0), new vscode.Position(this.text.length, this.text.length));
	rangeIncludingLineBreak!: vscode.Range;
	firstNonWhitespaceCharacterIndex!: number;
	isEmptyOrWhitespace!: boolean;
}


suite('provideDocumentSymbols関数', () => {
	vscode.window.showInformationMessage('Start all tests.');
	test('正常系 ifタグ[]', () => {
		//値定義
		const document = new MockTextDocument();
		const token = new MockCancellationToken();
		const op = new OutlineProvider();
		const excepted = Object.defineProperty(document.lineAt(0), 'text',{
			value:"[if exp=\"true\"]",
			writable:false,
		});		

		//実行
		let actual = op.provideDocumentSymbols(document,token)!;

		//アサート
		assert.strictEqual(actual[0].name, excepted.text);
	});
});


```


# 他の拡張機能を無効にする

argsに"--disable-extensions"を指定すれば良いです。
テスト実行時は他の拡張機能が動作しなくなります。

``` json:launch.json

		{
			"name": "Extension Tests",
			"type": "extensionHost",
			"request": "launch",
			"args": [
				"--disable-extensions",
				"--extensionDevelopmentPath=${workspaceFolder}",
				"--extensionTestsPath=${workspaceFolder}/out/test/suite/index"
			],
			"outFiles": [
				"${workspaceFolder}/out/test/**/*.js"
			],
			"preLaunchTask": "${defaultBuildTask}"
		}
```
# 参考サイト

https://code.visualstudio.com/api/working-with-extensions/testing-extension