MochaとChaiでなんでもテスト 6本目です。

今回は、ブラウザでしか動かないJavaScriptのコードをMochaとChaiでテストしてみます。

[ Windows 10 / mocha@6.2.2 / chai@4.2.0 / Google Chrome 78.0.3904.97 ]

  1. 前書き
  2. サンプル
  3. サンプル (モジュール版)

前書き

昨日調べてて知ったのですが、Mochaはブラウザでも動かせるんですね。普通に公式サイトに書いてありました。

ブラウザで動くと言っても、MochaのためにHTMLを一枚用意してその上で動かす感じなので、あくまでモジュールテストの用途になると思います。でもブラウザでしか動かないようなコード、例えばDOMいじりだとか Blob 周りだとかのテストができるのは嬉しいです。

それと特筆すべき点として、ブラウザで動かすMochaとChaiは unpkg.com から読み込むので npm install が不要です。Node.jsが入ってないPCでも動かせるので、ちょっとMochaとChaiを試してみたい人にもいいかもしれないです。

以下、この記事では公式サイトを参考に、適当なCanvasのユーティリティ関数をテストしてみます。

サンプル

サンプルのディレクトリ構成は以下の通りです。

 ...
  ├ tests
  │  ├ runner.html (テスト実行用HTML)
  │  └ browser.js (テストコード)
  └ code.js (テスト対象モジュール)

上に書いた通り、今回は npm install 不要なので node_modules はありません。

各ファイルの内容は以下の通りにします。

code.js (テスト対象モジュール)
/**
 * Canvasを作成
 * @param {Number} width 
 * @param {Number} height 
 * @return {HTMLCanvasElement}
 */
function createCanvas(width, height) {
	const canvas = document.createElement('canvas');
	canvas.width = width;
	canvas.height = height;
	return canvas;
}

/**
 * Canvasの座標からインデックスを算出する
 * @return {Number}
 */
function coordsToIndex(canvas, x, y) {
	if (x >= canvas.width) {
		throw RangeError('X座標がCanvasの範囲外です。');
	} else if (y >= canvas.height) {
		throw RangeError('Y座標がCanvasの範囲外です。');
	}
	return y * canvas.width + x;
}
tests/browser.js (テストコード)
const expect = chai.expect;  // chaiとmochaはrunner.htmlでグローバルに読み込み済み

describe('ブラウザでしか動かないテスト', function() {
	it('createCanvas', function() {
		const canvas = createCanvas(100, 200);
		expect(canvas).a('HTMLCanvasElement');
		expect(canvas.width).equal(100);
		expect(canvas.height).equal(200);
	});
	it('coordsToIndex (正常)', function() {
		const canvas = createCanvas(3, 2);
		expect(coordsToIndex(canvas, 2, 0)).equal(2);
		expect(coordsToIndex(canvas, 0, 1)).equal(3);
		expect(coordsToIndex(canvas, 1, 1)).equal(4);
		expect(coordsToIndex(canvas, 999, 999)).equal('※エラーになります');
	});
	it('coordsToIndex (例外)', function() {
		const canvas = createCanvas(3, 2);
		expect(() => { coordsToIndex(canvas, 3, 0); }).throw(RangeError);
		expect(() => { coordsToIndex(canvas, 0, 2); }).throw(RangeError);
	});
});
tests/runner.html (テスト実行用HTML)
<!DOCTYPE html>
<html lang="ja">
<head>
	<meta charset="utf-8" />
	<title>Mocha Tests</title>
	<link rel="stylesheet" href="https://unpkg.com/mocha/mocha.css" /> <!-- テスト結果表示に使用されるCSS -->
</head>
<body>
	<!-- テスト結果表示エリア -->
	<div id="mocha"></div>

	<!-- MochaとChai読み込み -->
	<script src="https://unpkg.com/mocha/mocha.js"></script>
	<script src="https://unpkg.com/chai/chai.js"></script>

	<!-- Mocha設定 -->
	<script class="mocha-init">
		mocha.setup('bdd');  // BDD用にセットアップする (describeとかitが使えるようになる)
		mocha.checkLeaks();  // テスト実行中にグローバル変数が追加されたらエラー終了させる
	</script>

	<!-- テスト対象モジュール、テストコード読み込み -->
	<script src="../code.js"></script>
	<script src="browser.js"></script> <!-- テストコード複数あるなら更に追記可 -->

	<!-- テスト実行 -->
	<script class="mocha-exec">
		mocha.run();
	</script>
</body>
</html>

一番特徴的なのは tests/runner.html です。tests/runner.html をブラウザで開くことでテストが実行され、結果が画面上に表示されます。

実行結果
実行結果

テストの再実行は tests/runner.html のリロードで行えます。

ただし今回はブラウザを使っているので、テスト対象モジュールまたはテストコードを修正してリロードしてもキャッシュが残ってる場合があります。常にスーパーリロード (Chromeなら Ctrl + F5) するか、キャッシュを無効にするなどして対策してください。

また、ここでは公式のサンプルと同様に mocha.checkLeaks() を有効にしてます。これを有効にしておくと、テスト実行中にグローバル変数が追加された時に global leaks detected エラーを吐くようになります。予期せぬグローバル汚染が検知できるので、基本的には有効にするのが良いと思います。

が、筆者環境ではChromeの拡張機能が勝手にグローバル変数を追加して、テストと関係無いところで以下のようなエラーを吐いたりしてました。

Error: global leaks detected: '__REACT_DEVTOOLS_COMPONENT_FILTERS__', '__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__'

今のところ『React Developer Tools』と『Vue.js devtools』が怒られてるのを見たので、入れてる方はテスト中は無効にするとかしておいた方がいいかもしれません。

サンプル (モジュール版)

上記のサンプルは、テスト対象モジュールをグローバルで読み込んでしまってます。気持ち悪いので、モジュール版サンプルも作ってみました。

ただしこちらはJavaScriptファイル内から別のファイルを読み込む関係上、tests/runner.html をURL file:///C:/~ で開いてるとCORS制約にひっかかってしまいます。これを回避するには tests/runner.html をローカルサーバー上などで動かす必要があるので、手軽さには欠けます。

ディレクトリ構成は同じですが、各ファイルの内容が少しずつ異なります。以下、コメントで★印をつけてるところが差分になります。

code.js (テスト対象モジュール)
/**
 * Canvasを作成
 * @param {Number} width 
 * @param {Number} height 
 * @return {HTMLCanvasElement}
 */
function createCanvas(width, height) {
	const canvas = document.createElement('canvas');
	canvas.width = width;
	canvas.height = height;
	return canvas;
}

/**
 * Canvasの座標からインデックスを算出する
 * @return {Number}
 */
function coordsToIndex(canvas, x, y) {
	if (x >= canvas.width) {
		throw RangeError('X座標がCanvasの範囲外です。');
	} else if (y >= canvas.height) {
		throw RangeError('Y座標がCanvasの範囲外です。');
	}
	return y * canvas.width + x;
}

export { createCanvas, coordsToIndex };  // ★モジュールなのでexportする
tests/browser.js (テストコード)
const expect = chai.expect;  // chaiとmochaはrunner.htmlでグローバルに読み込み済み
import { createCanvas, coordsToIndex } from '../code.js';  // ★テスト対象モジュールはここでimportする

describe('ブラウザでしか動かないテスト', function() {
	it('createCanvas', function() {
		const canvas = createCanvas(100, 200);
		expect(canvas).a('HTMLCanvasElement');
		expect(canvas.width).equal(100);
		expect(canvas.height).equal(200);
	});
	it('coordsToIndex (正常)', function() {
		const canvas = createCanvas(3, 2);
		expect(coordsToIndex(canvas, 2, 0)).equal(2);
		expect(coordsToIndex(canvas, 0, 1)).equal(3);
		expect(coordsToIndex(canvas, 1, 1)).equal(4);
		expect(coordsToIndex(canvas, 999, 999)).equal('※エラーになります');
	});
	it('coordsToIndex (例外)', function() {
		const canvas = createCanvas(3, 2);
		expect(() => { coordsToIndex(canvas, 3, 0); }).throw(RangeError);
		expect(() => { coordsToIndex(canvas, 0, 2); }).throw(RangeError);
	});
});
tests/runner.html (テスト実行用HTML)
<!DOCTYPE html>
<html lang="ja">
<head>
	<meta charset="utf-8" />
	<title>Mocha Tests</title>
	<link rel="stylesheet" href="https://unpkg.com/mocha/mocha.css" /> <!-- テスト結果表示に使用されるCSS -->
</head>
<body>
	<!-- テスト結果表示エリア -->
	<div id="mocha"></div>

	<!-- MochaとChai読み込み -->
	<script src="https://unpkg.com/mocha/mocha.js"></script>
	<script src="https://unpkg.com/chai/chai.js"></script>

	<!-- Mocha設定 -->
	<script class="mocha-init">
		mocha.setup('bdd');  // BDD用にセットアップする (describeとかitが使えるようになる)
		mocha.checkLeaks();  // テスト実行中にグローバル変数が追加されたらエラー終了とする
	</script>

	<!-- ★テストコードを type="module" で読み込み -->
	<script type="module" src="browser.js"></script> <!-- テストコード複数あるなら更に追記可 -->

	<!-- ★type="module" でテスト実行 -->
	<script type="module" class="mocha-exec">
		mocha.run();
	</script>
</body>
</html>

こっちの方が tests/runner.html でテスト対象モジュールを読み込まずに済むので、Node.jsでMochaを使う時と近い感じになりますね。

以上

Mochaのこの機能を使ったテストは今やってる最中なので、もっと何かあったら追記します。

次回はたぶんAWS Lambda編です。