先日、Next.js におけるテスト手法について、公式ドキュメントが追加され話題になりました。
https://twitter.com/delba_oliveira/status/1427307677709967362
取り上げられている 2 者はよく知られており、いずれかに触れたことがある方も多いかと思います。この公式ドキュメントページでは「何を使って」を紹介しているのみなので、どちらを選ぶべきか悩んだ方もいるのではないでしょうか?
この判断についてはドキュメントに書かれていなかったので、筆者なりの見解を紹介していきたいと思います。
Cypress は GUI が素晴らしく、テストを書く環境としてはとても体験が良いです。しかしテストが増えていくにつれ、以下のような点で DX 低下を招くことがあります。
cypress open
では成功していたテストがcypress run
では失敗するnext dev
では成功していたテストがnext start
では失敗するいわゆる「Flaky Test」になりがちで、Cypress バージョンを上げただけでテストが通らなくなった、ということも筆者は経験しました(not beaking change 対応漏れ)Flaky ではなかったとしても、実行コスト・実行時間に関してはどうしようもないので「なるべく Jest & React Testing Library に寄せた方が良い」と考えています。
「寄せる」という言葉のとおり、どちらでも担保可能なテストケースは多いです。「Jest & React Testing Library だけでもここまで担保可能」という例を見ていきましょう。
Testing Library を利用するとレンダリング結果をテスト可能とするだけでなく、Component 同士の結合テストも可能になります。ブラウザ E2E と同じように、クリックしたり input 要素に入力したり、というユーザー操作をエミュレートすることができます。
次の例では、Form のバリデーションエラーが表示されることをテストしています。この様に、フロントエンド側がバリデーションロジックを抱えている場合などには、Component 仕様を明文化する目的としても役立ちます。
import{ render, waitFor}from"@testing-library/react";import{ BirthdayInput}from"@/components/molecules/BirthdayInput";describe("molecules/BirthdayInput",()=>{describe("不正な値を入力し、送信を試みた時",()=>{beforeEach(()=>{const{ findByPlaceholderText, getByRole}=render(<BirthdayInput/>);const input=awaitwaitFor(()=>findByPlaceholderText("誕生日"));const button=awaitwaitFor(()=>getByRole("button")); fireEvent.change(input,{ target:{ value:"abcde"}}); fireEvent.click(button);});test("エラー文言が表示される",async()=>{awaitwaitFor(()=>expect(screen.getByRole("入力形式が不正です")).toBeInTheDocument());});});});
Context に依存した機能も、Jest & React Testing Library で担保できます。MSW などでモック API レスポンスを用意しておけば、SWR や React Query などを含んだ、Render as you Fetch な Component のテストも可能です(MSW に関してはこちらで解説しています)。
プロダクションコードとは「別の」Provider ファクトリ関数(下記withProvider
関数)をテスト向けに用意し、Context に保持している状態初期値を注入しテストします。Context に初期値を注入する構造はこちらの記事で解説しているとおりで、記事内にある様な GlobalUI の結合テストも、ここで担保することが出来ます。
import{ render, waitFor}from"@testing-library/react";import{ MyProvider}from"@/components/providers/MyProvider";import{ MyTemplate}from"@/components/templates/MyTemplate";import{ SWRConfig}from"swr";describe("templates/MyTemplate",()=>{constwithProvider=(children: React.ReactNode, user: User)=>render(<SWRConfig value={{ dedupingInterval:0}}><MyProvider user={user}>{children}</MyProvider></SWRConfig>);test("MyProvider の値が注入されている",async()=>{const{ getByText}=withProvider(<MyTemplate/>,{ name:"takepepe"});awaitwaitFor(()=>expect(getByText("Hello takepepe!")).toBeInTheDocument());});});
Jest & React Testing Library を併用していれば、Cypress で担保するテストケースは絞られてきます。以下は、API レスポンス status に応じて、ページタイトルを出し分ける Next.js のページ例です。ここで注目する点は next/head によるタイトルの動的書き換えです(API レスポンスのインターセプトは Jest & React Testing Library でも可能)
describe("templates/MyTemplate",()=>{const page="/users/takepepe";describe("正常レスポンスの場合",()=>{describe("ページに遷移すると",()=>{beforeEach(()=>{ cy.visit(page);});it("ユーザー名が含まれたタイトルになる",()=>{ cy.title().should("eq","takepepe の紹介ページ");});});});describe("ユーザーが退会している場合",()=>{beforeEach(()=>{ cy.intercept("GET","/api/users/*",{ statusCode:410});});describe("ページに遷移すると",()=>{beforeEach(()=>{ cy.visit(page);});it("退会ユーザー画面のタイトルになる",()=>{ cy.title().should("eq","このユーザーは退会しています");});});});});
他にも Cypress でしか担保出来ないテストケースは、複数画面を横断する機能、ブラウザ API に依存する機能などです。
Next.js 公式ドキュメントにも「推奨」として書かれていますが、Cypress の実行対象はnext dev
で立ち上げた開発サーバーではなく、next build && next start
で立ち上げたプロダクションビルドに近いアプリを対象にしましょう。