はじめに
最近の尋常ではない暑さに、ついに日傘を買った宮本です。
今回は、Astroでのコンポーネントテストについて紹介します。ただし、あくまでExperimentalな機能を使ったものであり、今後変更される可能性があるのでご注意を。
これまでコンポーネント単体をレンダリングする手段のなかったAstroですが、ついに今年の5月にリリースされた4.9にて、コンポーネント単体をレンダリングできるContainer APIがExperimental機能として追加されました。あくまでExperimentalということもあり、軽く触った感じだとやや機能が足りない部分を感じましたが、それでもコンポーネントテストが不可能だったこれまでに比べると大きな進歩です。
それはそれとして、この記事を書こうと思いながらもサボっていたら、いつの間にか4.12までAstroのバージョンが進んでいました。リリース頻度がとても早いです。
環境
- Astro: v4.12.3
- Vitest: 2.0.3
- node-html-parser: 6.1.13
今回の記事は、Astro公式で提供されている、Vitestを利用したテストテンプレートを元にファイルを追加して作成しました。手元ですぐに再現できるので、気になった方はぜひ試してみてください。
1 |
npm create astro@latest -- --template with-vitest |
コンポーネントテスト
次のようなCardコンポーネントをテストします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
--- // src/components/Card.astro type Props = { title: string; }; const { title } = Astro.props; --- <div> <h3> {title} </h3> <div class="card-body"> <slot /> </div> </div> |
Cardコンポーネントにはslotとpropsがあるため、テストではこれが正常に記述されているかを確認してみます。
リリース時の4.9.0だとpropsが指定できませんが、4.9.2からはpropsの指定も可能になっています。また、そのほかにもページルーティング時に利用するparamsなども指定可能です。レンダリング時に指定可能なオプションは公式ドキュメントから確認できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// src/components/Card.spec.astro import { experimental_AstroContainer as AstroContainer } from 'astro/container'; import { expect, test } from 'vitest'; import Card from './Card.astro'; test('Card with slots and props', async () => { const container = await AstroContainer.create(); // resultにはコンポーネントをHTMLとして出力した文字列が挿入される const result = await container.renderToString(Card, { props: { title: 'タイトル' }, slots: { default: 'コンテンツ' } }); expect(result).toContain('タイトル'); expect(result).toContain('コンテンツ'); }); |
このとき、resultにはコンポーネントをhtmlとして出力した場合の文字列が挿入されています。そのため実施しているのは、あくまで文字列の比較テストであるという点に注意が必要です。
上記のテストでresultに入る文字列は以下のようなものです(data-astro-source-fileの内容は変更してます)。テストの途中でconsole.logなどを使って出力するとわかりやすいです。
1 |
<!DOCTYPE html><div data-astro-source-file="~/vitest-astro/src/components/Card.astro" data-astro-source-loc="8:6"> <h3 data-astro-source-file="~/vitest-astro/src/components/Card.astro" data-astro-source-loc="9:7"> タイトル </h3> コンテンツ </div> |
見てわかる通り、出力されるhtml文字列はローカルのdevサーバを立ち上げた場合と同じものです。そのため、data属性として data-astro-source-file
や data-astro-source-loc
が自動的に含まれたタグが出力されています。本番環境と完全に同じではないため、注意が必要です。
これを削除したい場合は、 vitest.config.ts
にてテスト時に開発ツールバー機能をオフにすることで表示されなくなります。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/// <reference types="vitest" /> import { getViteConfig } from "astro/config"; export default getViteConfig( { test: {}, }, { // 開発時のツールバー機能を無効化 devToolbar: { enabled: false }, } ); |
1 |
<!DOCTYPE html><div> <h3 data-testid="title"> タイトル </h3> <div class="card-body" data-testid="body"> コンテンツ </div> </div> |
また、それ以上に文字列の比較になってしまうため、繰り返しなどを使っていて同じ文字列が複数表示されている場合に、コンポーネントの想定通りの場所にpropsやslotで挿入されているか確実とは言えない点も厳しいです。
そこで、文字列として出力されたhtmlをパーサーを使って解釈し、もう少し精度の高いテストをしてみます。今回はパーサーとしてnode-html-parserを利用したテストに変更してみました。
1 |
yarn add -D node-html-parser |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// src/components/Card.spec.astro import { experimental_AstroContainer as AstroContainer } from 'astro/container'; import { expect, test } from 'vitest'; import Card from './Card.astro'; import { parse } from 'node-html-parser'; test('Card with slots and props', async () => { const container = await AstroContainer.create(); // resultにはコンポーネントをHTMLとして出力した文字列が挿入される const result = await container.renderToString(Card, { props: { title: 'タイトル' }, slots: { default: 'コンテンツ' } }); const root = parse(result); const title = root.querySelector('h3'); const body = root.querySelector('.card-body'); expect(title?.innerText).toContain('タイトル'); expect(body?.innerText).toContain('コンテンツ'); }); |
パーサーを用いてhtml文字列を解析することで、より厳密にテストすることができるようになったと思います。もちろんdata-testidのようなテスト用のデータ属性を与えてテストすることも可能です。
注意点
いささかしつこいくらいの注意点になりますが、前提として現在のContainer APIはあくまでExperimental機能であり、今後変更される可能性があります。RFC内でも、小さな機能としてリリースしてからフィードバックをもとに改善していきたいと記述されています。
テストから話は離れますが、このContainer APIをもとにAstroのStorybook対応をするような展望もあるようなので、それに合わせて変更がある可能性もあります。(むしろ、テスト以上にStorybook対応を楽しみにしていた人も多い気がしています。が、これも今のContainer APIですぐに対応するのは難しい模様……。残念です)
また、それ以外にもいくつか現状だとテストできないことがあります。
クライアント側で動作するscriptタグ(とstyleタグ)は、html文字列には含まれません。is:inlineを指定すれば含まれますが、これをテストするのはなかなか厳しいと思います。
クライアント側で動作するロジックをテストしたい場合は、関数として別ファイルで定義し、コンポーネント側ではscriptタグ内でimportして利用するのが現実的だと思います。
1 2 |
// src/lib/math.tsで定義して別途テストする export const add = (a:number,b:number) => a+b; |
1 2 3 4 5 |
<script> // コンポーネント側ではimportして利用 import {add} from "../lib/math.ts"; console.log(add(1, 2)); </script> |
もっとも、それでもテストし辛いような複雑なDOM操作やクラス追加などを実施するロジックがある場合は、そもそも部分的にReactやSvelteで作成したコンポーネントを用いるのが良いかもしれません。目的に応じてコンポーネントをAstro以外で作成することができるのもAstroの良い点です。
一方で、Astroコンポーネント内でReactやSvelteを利用している場合は、また別途設定が必要になるため注意が必要です。
さいごに
今回はAstroでのコンポーネントテストについて紹介しました。
もともとAstroにはコンポーネント単体を独立してレンダリングする機能がなく、そのためコンポーネントテストができないという課題がありましたが、Container APIの追加によって実現できることが増加しました。
まだできることも少ないですが、Astroは半月に1回ほどのペースで頻繁に機能追加を伴うリリースが行われているため、今後がより楽しみです。
参考
- Astro 4.9 | Astro
- Astro Container API (experimental) | Docs
- data-astro-source-file added when devToolbar disabled · Issue #9324 · withastro/astro
- node-html-parser – npm
- roadmap/proposals/0048-container-api.md at rfc/container-api · withastro/roadmap
- Support for Astro components · Issue #18356 · storybookjs/storybook