FreshのIslandコンポーネントをテストする
#Fresh #Preact #Preact_Testing_Library
はじめに
FreshのIslandコンポーネントのテストを書けないか試してみたので、公式のサンプルであるCounterコンポーネントを例に方法について紹介します。
追記
このページで試してみたことをベースにfresh-testing-libraryというライブラリを作ってみました。
前提
このページは以下のバージョンを想定して書かれています。
Deno v1.34.0
Fresh v1.1.6
コード
以下のような形でテストを用意してみたところ、うまくいきました。
それぞれ解説していきます。
code:test/islands/Counter.test.tsx
import Counter from "../../islands/Counter.tsx";
import { assert } from "https://deno.land/std@0.190.0/testing/asserts.ts";
// (1)
import {
afterEach,
beforeAll,
describe,
it,
} from "https://deno.land/std@0.190.0/testing/bdd.ts";
// (2)
import {
cleanup,
fireEvent,
render,
} from "https://esm.sh/@testing-library/preact@3.2.3?external=preact";
import { JSDOM } from "https://esm.sh/jsdom@22.1.0?no-dts";
import vm from "node:vm";
// (3)
beforeAll(() => {
// NOTE: Deno v1.34時点では未実装のため
const isContext = vm.isContext;
vm.isContext = () => false;
const { document } = new JSDOM().window;
globalThis.document = document;
vm.isContext = isContext;
});
// (4)
afterEach(cleanup);
// (5)
describe("Counter", () => {
it("should work", async () => {
const screen = render(<Counter start={10} />);
assert(screen.queryByText("10"));
const plusOne = screen.getByRole("button", { name: "+1" });
const minusOne = screen.getByRole("button", { name: "-1" });
await fireEvent.click(plusOne);
await fireEvent.click(plusOne);
assert(screen.queryByText("12"));
await fireEvent.click(minusOne);
assert(screen.queryByText("11"));
});
});
(1) std/testing/bdd.tsについて
これはDenoの標準ライブラリであるdeno_stdで提供されているモジュールの一つで、describe()やit()などのAPIを使ってテストケースを記述することができます。
std/testing/bdd.tsで書かれたテストケースは、通常通りdeno testコマンドで実行することができます。
(2) Preact Testing Libraryを読み込む
esm.shから@testing-library/preactを読み込んでいます。
code:tsx
import {
cleanup,
fireEvent,
render,
} from "https://esm.sh/@testing-library/preact@3.2.3?external=preact";
npm:@testing-library/preactでも読み込むことは可能だと思いますが、esm.shを使用しているのは、Freshは各種依存関係をImport mapsで管理していることが理由です。
?external=preactの指定が重要な部分で、このパラメータで指定されたパッケージについてはesm.shはImport Specifierの書き換えを行わず、そのまま維持してくれます。
これによりImport mapsで定義されているpreactがそのまま読み込まれるようになります。
code:import_map.json
{
"imports": {
"$fresh/": "https://deno.land/x/fresh@1.1.6/",
"preact": "https://esm.sh/preact@10.13.1",
...
}
}
(3) globalThis.documentをセットアップする
次に、Preact Testing Libraryを動かすために、jsdomを使用してglobalThis.documentを設定します。
注意点として、jsdomはnode:vmのisContextなどのAPIに依存していますが、Deno v1.34時点ではまだ未実装のため、一時的に置き換えを行っています。
code:typescript
beforeAll(() => {
// NOTE: Deno v1.34時点では未実装のため
const isContext = vm.isContext;
vm.isContext = () => false;
const { document } = new JSDOM().window;
globalThis.document = document;
vm.isContext = isContext;
});
(4) 各テストケースの終了時にcleanup()を呼ぶ
Jestなどを使用している場合、Preact Testing Libraryは各テストケースの終了時に自動でcleanup()を呼んでくれます。(コード)
しかし、Denoの場合は自動では呼ばれないので、自前で呼ぶ必要があります。
code:typescript
afterEach(cleanup);
(5) テストコード
以上のセットアップなどを行っておけば、Node.jsの場合と同様にテストが書けそうです。
code:tsx
describe("Counter", () => {
it("should work", async () => {
const screen = render(<Counter start={10} />);
assert(screen.queryByText("10"));
const plusOne = screen.getByRole("button", { name: "+1" });
const minusOne = screen.getByRole("button", { name: "-1" });
await fireEvent.click(plusOne);
await fireEvent.click(plusOne);
assert(screen.queryByText("12"));
await fireEvent.click(minusOne);
assert(screen.queryByText("11"));
});
});
テストを実行する際は、通常通り、deno testコマンドで実行できます。
code:shell
$ deno test ./test/islands/Counter.test.tsx
補足
DOMライブラリについて
将来的にはdeno-domなども選択肢になりそうですが、現状ではgetComputedStyle()あたりが未実装のようで、Preact Testing Libraryを動かすのが難しそうでした。
そのため、現時点ではjsdomやhappy-domあたりを使う必要がありそうです。
アサーションについて
試していないのですが、もしかしたら、以下のあたりを使うとexpect()を使って書ける可能性があるかもしれません。
@vitest/expect
@testing-library/jest-dom (#427)
依存管理について
説明のため、このページではdeno_stdなどのURLを直接指定してimportしていますが、実際に使用する際はImport mapsで依存管理するとよさそうです。
code:import_map.json
{
"imports": {
"$fresh/": "https://deno.land/x/fresh@1.1.6/",
"preact": "https://esm.sh/preact@10.13.1",
...
"std/": "https://deno.land/std@0.190.0/",
"@testing-library/preact": "https://esm.sh/@testing-library/preact@3.2.3?external=preact",
"jsdom": "https://esm.sh/jsdom@22.1.0?no-dts"
}
}