https://zenn.dev/takepepe/articles/nextjs-testing-strategy-2022
近日連投していた Next.js 記事のサンプルコードを公開しました。このサンプルコードを元に、私のフロントエンドディレクトリ構成・テスト観点を紹介します(あくまで執筆現在の脳内アウトプットになりますのでご了承ください)
Components のディレクトリ構成はいつも悩みのタネです。このモジュールシステムは「デザインシステム観点・アクセシビリティ観点・フロントエンド実装観点」の 3 つの観点が混在するため事情が複雑です。しかし、どうせ作るのなら「デザイナー・フロントエンド」どちらの開発基盤にもなりえる、モジュールシステムを目指したいところですよね。
筆者は 「迷ったらアクセシビリティ軸で判断する」 ようにしています。実装とテストコードを突き合わせると「そこに分類する確かさ」に、自信がもてるようになります(これは UI コンポーネントに限った話ではありません)フロントエンド UI コンポーネントテストは「role 属性&アクセシブルネーム」でクエリーを書くのが現在主流です。そのため、アクセシビリティ観点で似たもの同士がグルーピングされることは、自然な流れだと考えています。
「意味的単一要素」 で構成されるコンポーネント置き場です。デザイン上では複合要素に見えても、意味的には単一要素として表現したいのであれば atoms と判断します。テストコードでは、意図した role 属性でクエリできるかを確認します。
describe("src/components/atoms/Button/Button.test.tsx", () => {
test("[role=button]であること", () => {
const { getByRole } = render(<Default />);
expect(getByRole("button", { name: "送信する" })).toBeInTheDocument();
});
});
describe("src/components/Textbox/Textbox.test.tsx", () => {
test("[role=textbox]であること", () => {
const { getByRole } = render(<Default />);
expect(getByRole("textbox")).toBeInTheDocument();
});
});
「意味的複合要素」 で構成されるコンポーネント置き場です。aria-describedby
やaria-errormessage
を用いて、内部で複数要素が関連付けられている場合、molecules と判断します。テストコードでは、関連付けが意図通り機能しているかを確認します。list
ロールなど、子要素を持つ前提のコンポーネントも molecules に分類します。
describe("src/components/TextboxWithTitle/TextboxWithTitle.test.tsx", () => {
const options: ByRoleOptions = { name: "お名前" };
test("labeltext が textbox のアクセシブルネームであること", () => {
const { getByRole } = render(<Default />);
const textbox = getByRole("textbox", options);
expect(textbox).toBeInTheDocument();
});
test("description で textbox が識別されていること", () => {
const { getByRole } = render(<HasDescription />);
const textbox = getByRole("textbox", options);
// aria-describedby が機能していることを確認
expect(textbox).toHaveAccessibleDescription("姓名を入力してください");
});
test("error で textbox が識別されていること", () => {
const { getByRole } = render(<HasError />);
const textbox = getByRole("textbox", options);
// aria-errormessage が機能していることを確認
expect(textbox).toHaveErrorMessage("入力エラーがあります");
expect(textbox).toBeInvalid();
});
});
「意味的主要要素」 を担うコンポーネント置き場です。Landmark Roles・Window Rolesなどが相当します(例外として、search ロール要素など、デザインシステム観点から見て organisms として不自然なものはここに置きません)また、後続の理由から、main ロールは organisms に含めないようにします。
describe("src/components/organisms/BasicAside/BasicAside.test.tsx", () => {
test("[role=complementary]であること", () => {
const { getByRole } = render(<Default />);
expect(getByRole("complementary")).toBeInTheDocument();
});
});
describe("src/components/organisms/BasicHeader/BasicHeader.test.tsx", () => {
test("[role=banner]であること", () => {
const { getByRole } = render(<Default />);
expect(getByRole("banner")).toBeInTheDocument();
});
});
templates は、pages と一対一になるコンポーネント置き場です。Next.js の getServerSideProps などで取得したデータを流し込みます。html ページには一つの<main>
タグを含めるべきなので、このコンポーネントのルートにすると合点がいきます。もし子コンポーネントに<main>
を含めてしまった場合は「複数要素がマッチした」という理由で、このテストは落ちるでしょう。
describe("src/components/templates/UserEdit/UserEdit.test.tsx", () => {
const server = setupMockServer(updateUserHandler());
test("main ランドマークを1つ識別できること", () => {
const { getByRole } = render(<Default />);
const main = getByRole("main");
expect(main).toBeInTheDocument();
});
});
テストを書くにしても、アクセシビリティツリーがよく分からない場合、開発ツールを少し掘り下げると理解がはかどると思います。先日この件について投稿しましたのでご参考まで。