はじめに
Visual Studio の 2022 以降、JavaScript の単体テストが Visual Studio のテストエクスプローラ上で簡単に検出・実行できるようになりました。これは Blazor 用の Razor クラスライブラリを開発する際に便利です。というのも、C# コードから呼び出す JavaScript ヘルパーコードを含む Razor クラスライブラリを開発しているとき、C# コードと JavaScript コードの両方の単体テストを実行したいと思うことがよくあったからです。Visual Studio 2022 で遂に、それが容易にできるようになりました。
どうやるの?
Razor クラスライブラリに対する、特に C# コードの単体テストプロジェクトが既にあるとします。
その場合、以下のようないくつかのステップを追加するだけで、その単体テストプロジェクト上で JavaScript コードのテストを追加で行うようにすることができます。
① 単体テストプロジェクトファイル (.csproj) に <JavaScriptTestFramework>
プロパティを追加し、どの JavaScript 単体テストフレームワークを使用するのかを指定します。また、JavaScript 単体テストのコード (.js ファイル) を配置する相対パスを指定するために <JavaScriptTestRoot>
プロパティを追加します。
以下は、JavaScript のテストフレームワークとして有名な "mocha" を使用する場合の例です。
<!-- 単体テスト・プロジェクトの.csprojファイル -->.
<Project Sdk="Microsoft.NET.Sdk">
...
<PropertyGroup>
<JavaScriptTestFramework>Mocha</JavaScriptTestFramework>
<JavaScriptTestRoot>tests\</JavaScriptTestRoot>
</PropertyGroup>
...
② JavaScript の単体テスト環境を構築します。例えば、"mocha "を使用する場合、テストプロジェクトディレクトリ上のターミナルコンソールで、以下のコマンドを実行します。
(前提として、その開発環境に Node.js がインストールされている必要があります)
> npm init -y
> npm install --save-dev mocha
また、単体テストプロジェクトから "node_modules "フォルダを以下のように除外しておきます。
<!-- 単体テスト・プロジェクトの.csprojファイル -->.
<Project Sdk="Microsoft.NET.Sdk">
...
<!-- 👇 "node_modules" フォルダを全ての処理対象から除外します。 -->
<ItemGroup>
<Compile Remove="node_modules\**" />
<EmbeddedResource Remove="node_modules\**" />
<None Remove="node_modules\**" />
</ItemGroup>
...
③ 使用する JavaScript 単体テストフレームワークに従って、単体テストのコードを記述します。
// ./tests/fooTest.js
const assert = require("assert");
describe("fooTest", function () {)
it('foo is bar', function () {)
assert.ok("this test will always pass.");
})
});
以上の手順で、Visual Studio 2022 のテストエクスプローラ上にこれら JavaScript 単体テストが表示され、Visual Studio の通常の方法でこれらの単体テストを実行することができます (下図例)。
詳しくは、以下の公式ドキュメントサイトへのリンクもご参照ください。
TypeScriptでテストコードを書く
もちろん、JavaScript の単体テストコードを、TypeScript で記述することもできます。具体的には、以下のような手順で記述します。
① 何らかの方法で、NuGetパッケージ「Microsoft.TypeScript.MSBuild」を単体テストプロジェクトにインストールします。単体テストプロジェクトディレクトリで、下記のとおり dotnet add package
コマンドを実行するのも一つの方法です。
> dotnet add package Microsoft.TypeScript.MSBuild
③ 必要な TypeScript コードの型定義ファイル (.d.ts) を、以下のように追加します。
> npm install --save-dev @types/mocha @types/node
③ TypeScript コンパイラの動作を設定するために、以下の内容の「tsconfig.json」ファイルを単体テストプロジェクトディレクトリに追加します。(「target」の設定は、必要に応じて変更してください)
// ./tsconfig.json
{
"compilerOptions": {
"target": "ES2017",
"module": "CommonJS",
"strict": true
},
"compileOnSave": true
}
以上で、単体テストコードを TypeScript で書けるようになります (下記例)。
// ./tests/fooTest.ts
import * as assert from "assert";
import { describe, it } from "mocha";
describe('fooTest', function () { )
it('foo is bar', function () {)
assert.ok("this test will always pass.");
})
});
問題発生! - 互換性のないモジュールシステム
ところで、私は Razor クラスライブラリ(このシナリオでの単体テスト対象)の中でも、ヘルパー JavaScript コードを TypeScript で書いています。
具体的には、Razor クラスライブラリプロジェクト内の tsconfig.json
は以下のような構成になります。
// Razor クラスライブラリプロジェクトにおける tsconfig.json
{
"compilerOptions": {
...
"module": "ES2015", // 👈 CommonJS ではなく、ES Module システムを使用します。
...
TypeScript コンパイル時のモジュールシステムに、(CommonJS ではなく) ES Module を指定していることに注目してください。
これは、そのヘルパー JavaScript コードは、Blazor アプリケーションからは IJSRuntime.InvokeAsync<IJSObjectReferense>("import", <The path of .js file>)
を使って読み込まれるためです。
結果として、コンパイルされた TypeScript のコードは、例えば "CatClass "の場合、次のような JavaScript コードになります。
// Razor クラスライブラリプロジェクト内の catClass.js
export class CatClass {
meows() {
return "Meow!";
}
}
ところがです。
残念なことに、なんと "mocha" は、Visual Studio のテストエクスプローラー上では、ES Module モードでは動作しないようなのです。
ここでは詳細は省略しますが、とにかく、Visual Studio のテストエクスプローラーで利用できる "mocha" 用の単体テストコードは、CommonJS モードに設定する必要があるのです。
例えば、単体テストコードの TypeScript で、テスト対象として "catClass.js" をインポートしている場合、以下のように記述するかと思いますす。
// 単体テストプロジェクト内の ./tests/catClassTest.ts
...
import { CatClass } from "../../RazorClassLibrary1/catClass";
describe('CatClass', function () { )
it('meows', function () {)
const cat = new CatClass();
assert.equal('Meow!', cat.meows());
});
});
しかしこの状態で TypeScript コンパイルを実行すると、Visual Studio テストエクスプローラーが単体テストを検出しようとする段階で、"SyntaxError.Unexpected token 'export'" エラーが発生してしまうのです。
(実は "mocha" それ自体は Ver.9 から ES Module モードでも動作するようになったそうです。しかしながら、Visual Studio のテストエクスプローラーがテストを検出・実行する際に、"mocha" の ES Module モードを有効にする方法がないようなのです)。
さてどうしたものでしょうか。
自分のの解決策 - 複雑だけど、とりあえず動く
幸いなことに、自分は JavaScript コードはすべて TypeScript のソースコードで書いています。
そこで、単体テスト用のファイルだけでなく、テスト対象のファイルも含めて、すべての TypeScript ファイルを「CommonJS」モジュールモードでコンパイルして、単体テストプロジェクトフォルダの下に配置することで、この問題を解決することにしました。
具体的に見ていきましょう。
まず、単体テストの TypeScript ファイルは、単体テストプロジェクトのルートフォルダ ( "./tests" フォルダではなく) に配置しました。
これは、単体テストコードと Razor クラスライブラリ中のコード(テスト対象)の両方の TypeScript ファイルを一度にコンパイルするために大事なポイントです。
もちろん、単体テスト用の TypeScript ファイルでは、テスト対象のモジュール(TypeScriptファイル)をインポートするパスも以下のように調整しました。
// 単体テストプロジェクト内の ./tests/catClassTest.ts を ./catClassTest.ts に移動します。
...
// 👇 テスト対象モジュールのインポートのパスも修正
//import { CatClass } from "../../RazorClassLibrary1/catClass";
import { CatClass } from "../RazorClassLibrary1/catClass";
...
次に、単体テストプロジェクトのルートフォルダにある TypeScript コンパイラの設定「tsconfig.json」に、2つのオプションを追加しました。
一つは、Razor クラスライブラリプロジェクト(テスト対象)のフォルダをコンパイル対象フォルダに追加するものです。
そしてもう一つは、単体テストプロジェクトファイル(.csproj) の <JavaScriptTestRoot>
プロパティ値に従うように、出力フォルダを明示的に設定することです。
// 単体テストプロジェクト内の ./tsconfig.json
{
"compilerOptions": {
...
// 👇この2つのオプションを追加します。
"rootDirs": [ "./", "../RazorClassLibrary1" ],
"outDir": "./tests"
...
下図は、上記の変更を行い、ソリューション全体をリビルドした結果の様子を示したところです。
これでようやく、Visual Studio のテストエクスプローラー上で、Blazor 用 Razor クラスライブラリに含まれるヘルパー JavaScript コードに対する単体テストをも、きちんと実行できるようになりました! 🎉
おわりに
Blazor 用の Razor クラスライブラリを開発する際、とくに、その Razor クラスライブラリにヘルパー JavaScript コードを含む場合に、Visual Studio のテストエクスプローラー上で、C# 用と JavaScript 用の単体テストを同時に実行できると非常に便利です。
しかし、JavaScript のテストフレームワーク "mocha" は、テスト対象の JavaScript コードのモジュールタイプが ES Module であるにもかかわらず、Visual Studio テストエクスプローラー上では CommonJS モジュールモードでしか動作させることができませんでした。
幸い、自分はいつも TypeScript で JavaScript コードを書いているので、テスト対象の TypeScript ファイルを、異なるモジュールモード(「ES Module」と「CommonJS」)で各々のプロジェクト用に 2回コンパイルすることで、この問題を解決しました。
サンプルコードは、以下のリンク先のGitHubリポジトリで公開してあります。
しかし、実のところ、この解決策がベストなのかどうか、確信はありません。
本当に Visual Studio テストエクスプローラー上では ES Module モードで "mocha" を実行する方法はないのでしょうか?
あるいは、Jest や Jasmine などの他の JavaScript テストフレームワークなら、 Visual Studio テストエクスプローラー上でも ES Module モードで動作させることができるのでしょうか?
もし、もっと良い解決策やアイディアがあれば、それを共有していただけると助かります。
Learn, Practice, Share. :)