ブラウザ上でJavaScriptの単体テストをしたい

単体テストはjest、E2Eテストならplaywright。

これが最近の流行なんじゃなかろうか。類に漏れず、私もその2つを使ってる。

ただこの前困ったことがあった。ブラウザでしか動かないコードをテストしたくなったのだ。


jestはnodeで実行されるから、nodeでもブラウザでも動くコードなら問題なくテストできるのだけれども、このAPIは実装されていないのでダメだし、ブラウザごとにアルゴリズムの実装状況が違うかもしれないから、それをブラウザごとにテストしたい。つまり、やりたいこととしては、単体テストなんだけど、実現方法としてはブラウザを起動して行うためe2eに近い。


jest前は、jasmin、karmaなどで、ブラウザ上でテストするのは一般的だったから、そのころと同じような構成とるかと思ったけれども、e2eようにplaywrightは入れてるわけで、そこに新たにpupetter入れてとかするのも、ことが大きすぎるなと感じる


結果的に考えたのは以下のような構成。


# dir tree
├ src
│	├ client
│	│	└ test_target.ts
│	└ server
├ test
│	├ build
│	├ www
│	│	├ index.html
│	│	├ index.js
│	│	└ required.js (download https://requirejs.org/docs/release/2.3.6/minified/require.js)
│	└ test.spec.ts
├ package.json
├ playwright.config.js


tscで直トランスパイルして、バンドラーは使わない。


// package.json
{
  ...
	"scripts": {
		...
		"build:testlib": "tsc ../src/client/*.ts --out test/build --target es2021 --module umd --skipLibkCheck",
		"test": "playright test"
	},
	...
}


そのため、テスト対象のファイルの依存関係は気を付ける。でも単体テストの対象なのでそこまで複雑なファイルにはならないはず。


// src/client/test_target.ts
export const TestTarget1 ...
export const TestTarget2 ...


tscのumdは、慣習的なumdと違ってglobalに吐き出してくれないのでrequirejsを使わないと読み込めないのでrequirejsとグローバルに吐き出すためのjsを用意する。

読み込み終わったら、`#done`が作られるようにしておく


// test/www/index.js
requirejs(["../build/client/test_target"], function() {
	window.Libs = arguments;
	document.body.insertAdjacentHTML('beforeend', `<h1 id="done">done</h1>`);
});
<!-- test/www/index.html -->
<!DOCTYPE html>
<html>
    <head>
        <script data-main="index.js" src="require.js"></script>
    </head>
    <body>
    </body>
</html>


jsファイルをrequirejsで読み込むさっきのHTMLを開いて、#doneが出てくるまで待てば、あとは、evaluateの中でコードが書ける。

型情報は死滅してるけど、やりたいテストはできてるし、間違ってたらテストでこけるのいいということにする。


// test/test.spec.ts
import { test, expect, Page } from '@playwright/test'
async function init(page:Page) {
	await page.goto(`file:///${__dirname}/www/index.html`);
	await page.waitForSelector('#done');
}

test('unittest on browser', async ({page}) => {
	await init(page);
	
	const result = await page.evaluate(() => {
		// @ts-ignore
		const { TestTarget1 } = Libs[0];

		return TestTarget1 !== undefined;
	});

	expect(result).toBe(true);
})