🏆

Jestのuiテストがつらすぎるので愚痴らせてください。そしてブラウザテストで本質的なuiテストをしよう

2023/06/17に公開
2

初めに、そして前提


現代ではReactでUIの単体テスト・インテグレーションテストを書く場合、Jest x React Testing Library を使うのが一般的かと思います。皆さんはJestでUIテストを書いていますか?Jestでコンポーネントの単体テストを書いていると辛いことがたくさんありませんか?例えば

  • 大量のライブラリのモックによる(これってテストやる意味あるの・・・?)と感じる虚無感
  • テストが落ちてもどこが原因なのか全然わからないデバッグのしずらさによる虚無感
  • ブラウザだと普通に動いてるのになんでJestだと同じ挙動しないんだという虚無感
    • 本来ユーザーはブラウザ経由でしかアプリケーションを見ないのに・・・

他にもきっといっぱいあるかと思います。もしこれらの虚無を感じたことがないあなたはきっと天才だと思います。
                        
ですが僕らにまとわりつく「ui部分だろうがちゃんと単体テストはしなきゃだめだろ常考(΄◞ิ౪◟ิ‵)」という固定観念はなかなかしぶといです。

なので頑張ってJestを使わない。ブラウザテストだけで完結する素晴らしいテストを模索してJestでコンポーネントのテストを書かないで済む理想を追い求めたいとこの記事を書きました。

以下注意点

  • 対象読者はフロントエンドエンジニア

    • フロントエンドでテストとか書いたことないわwwという人はこれを気にUIテストをPlaywrightで始めて見てください!
  • 単体テストとインテグレーションテストがこんがらがっちゃった記載があります

    • 要はJestのUIテストが辛いということを言いたいだけなので、いい感じにスルーしてください
  • Jest自体のディスではなく、jsdomとかjest-environment-jsdomによってレンダリングされたuiのテストが虚無という話です

    • 関数テストとかは全然いいと思います
    • renderHookとかによるhooks onlyのテストも全然いいと思います

以下 事前想定コメント返答集

  • いや、JestのUIテストとか簡単だろwww
    • あなたは天才なので自信を持ってください
  • お前のコードがクソなのがいけないwww
    • コメントでいい感じの書き方教えてください・・・Jest難しい
  • 気に入らなければコントリビュートすればええやんwww
    • う、うぅ・・・おっしゃるとおり・・・愚痴ることしかできない人でごめんなさい・・・

あと、ここが一番重要です。筆者はすべてのOSS制作者を尊敬しているのでjsdomのディスりをしててもそれの制作に関わったすべてのコントリビュータへのリスペクトはちゃんと持っています(持っているつもりです)

なんのお話する?

  1. 初めに、そして前提(上で書いたやつなので既読済)
    前提条件の共有をして認識を合わせます。

  2. jestのuiテストの辛みを永遠に列挙します
    みんなも絶対経験したことがあるだろうハマりポイント、虚無ポイントを列挙します。共感してくれるはず・・・。
    ※注意点: react前提での話です

  3. 本来のuiテストで調べないといけない観点を考察します
    個人的感想(以下略

  4. ブラウザテスト最高!という話をオタク特有の早口でペチャクチャ
    「(΄◞ิ౪◟ิ‵)< ニチャァ」 ってなりながら喋ってます

  5. 現代のブラウザテストの構成とテスト手法
    俺の考えた最強のツール選定と、何をテストするのか、という観点の解説です

  6. ブラウザテストの辛みポイント・・・
    理想と現実ってやつ

  7. 最後に、そしてポエム
    uiテストを書いているすべての人類へ・・・

jestのuiテストの辛み

永遠に列挙していきます。できるだけcodesandboxなどで再現できるよう努めますが、体力が尽きたら途中から文章だけに留めちゃいます。(こっそりsandboxを追記する場合もあります)

フレームワーク独自の仕様を絡めたテストするのが難しい

1例としてNext.jsの App Routerのページレンダリング機構を取り上げます。
ここではApp Layoutの具体的な内容は取り上げません。ドキュメント見てくださいね。

https://nextjs.org/docs/app/building-your-application/routing/pages-and-layouts

Next.jsではNestされたlayout.tsxやpage.tsxを絡めて最終的にレンダリングされます。これをテストしたいと考えたとします。
もうこの時点で普通にコンポーネントをレンダリングするだけでは動きません。

結合テストに挑戦してる記事を見つけましたが、ここにはいくつかの懸念点があります。
※執筆当時はまだalphaだったので今ここで言ってもセコい感はありますがこれ以上にちゃんと書いてある記事が見つかりませんでした。

https://zenn.dev/takepepe/articles/nextjs13-components-integration-test

  • あくまで「擬似的に」再現してるだけ、という点
  • フレームワークのアップデートに追従しないといけないのでメンテナンス力が試される
    • 昔next-page-testerというNext.jsのpageレンダリングを再現しようとするOSSがありましたが筆者がアップデートを中止しました
    • 該当のissueを見てみるとかなり辛さがあったんだろうな、と思わせてくれます。

デバッグがウルトラスーパーデラックス難しい

JestのUIテストではテストが落ちたとき、どんな画面表示になっているのかわかりません。分かるのはテストが落ちた時点でのdomのスナップショットくらいです。。。
ossにjest-previewというツールがあり、テストが落ちたときや手動で実行した際にjest-domにあるdomのスナップショットをブラウザで見れるようにしてくれるツールがあります。これのお陰で相当デバッグはやりやすくはなりました。
https://www.jest-preview.com/

しかしuiというものはコマ送りで進むものではありません。ヌルヌル動いているんです。バグが起きたときに静的なdomのスナップショットを見せられてもわからないことがたくさんあります。jestのみで発生するバグなんかを引いた日にはそれはもう・・・地獄・・・

ブラウザでは動いてるのにJestでは動かない、の罠

window apiなどが使えない

window api + jestで検索するとHow to Mockみたいな記事が大量に出てきます。
https://www.google.com/search?q=ResizeObserver+jest
https://www.google.com/search?q=Intersection-overser+jest

jest-domではこういったwindow apiは手動でモックする必要があります
https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom

こういったuiのコア機能をモックしてできるテストがどれだけ信頼性があるのか、これを考えると虚無感がすごいことになっちゃいます。虚無

ReactHookFormでuncontrolledなformの入力が画面に反映されなかった(only react)

※これだけは後日必ずsandboxに書いておきます。もし再現しなかったら打消し線でこの部分消します

※追記(2023/06/18)
手を動かしてcodesandboxで最小構成で試してみたのですがまっったくバグが再現しませんでした。当時とバージョンが違っているのでその可能性もあるかもしれないですが、多分当時の現象は別の事象が原因だったのかな?って思います。手元で再現してから記事に書くべきであると反省しました。以後気をつけます

これはここ1年で見つけた現象で、他に書いてある記事がなかったのでせっかくなので書きます

reactのuseFormで帰ってくるregister関数を当て込んだ inputタグにuser-eventで文字を入力します。それに対して testing-libraryのexpect(inputElement).toHaveValue("入力文字")を使ってクエリを当てはめると何故かテストが落ちます。jest-previewでも入力文字が表示されません。ちなみに当たり前ですがブラウザテストでは普通に通ります。もっと言うとhandlesubmitするとちゃんと入力文字が取り出せるようになってます。え?

根本原因はわかりませんが、どうやらinputタグにvalueアトリビュートが入っていないから落ちたっぽいですね。testing-library的にはvalueがあるかどうかの判定にvalueアトリビュートを見るのはごくごく自然なことなので、testing-library自体には恨みはありません。そしてreact-hook-form内部にはデータがあるのでhandlesubmitするとデータが取り出せる。こういう理屈っぽいです。虚無感が。

ワークアラウンドによる大量の虚無モック

ライブラリのissueを見るとテスト時の不具合が紹介され、ワークアラウンドでmock方法が書いてある。それをコピペしてきたら動くようになった。皆さんもそんな経験ありませんか?
このモックがsetupファイルに大量に書かれてるのを見ると虚無感が・・・

単体テストを成立させたり、カバレッジを満たすために行う虚無モック

皆さんはしたことありませんか?自分はこれのためだけに数十分、下手したら1時間位悩んでしまったことも数少なくありません。とくにComponent内部のHooksのモックをしようとすると超しんどいです。renderHook単体で済んだり関数onlyだけのテストコードならいいですが、コンポーネントで実際に使われている挙動をチェックする必要があったりする場合は・・・

本来のuiテストで調べないといけない観点の考察

ユーザーはjsdomでアプリケーションを動かしません。ブラウザで動かすはずです。当たり前のことは省いて、本来ここを調べなきゃいけないよね?で筆者が特に思った点を挙げてみます

ブラウザごとの挙動・UIの確認

Safari・Firefox・Google Chrome、イ、インターネットエクスプローラー...etc、色々なブラウザがあります。これらのブラウザは内部でWebkitだったりGeckoだったり様々なHTMLレンダラーを介してレンダリングを行います。
javascriptやcssは特にブラウザごとに使用できるapiがあったりなかったりとかなりカオスですよね。そしてバージョンによって廃止されたり・追加されたりとか、mdnとか見るとよく分かるかと思います。
例: こういうの https://developer.mozilla.org/ja/docs/Mozilla/Add-ons/WebExtensions/Browser_support_for_JavaScript_APIs#action

jestで無理やりカバレッジを確保するための虚無モックを必死こいて書く前にこういった観点でのテストを書いたほうが生産的だと思っています。ですよね?

(現実的にするのは難しいと思うけど)ブラウザバージョンごとの動作確認

ブラウザの種類だけでなくバージョンごとの確認も必要です。なぜならば前述の通り、特定のバージョンでは機能が剥がされたり、古いバージョンでは搭載されていないcss,javascriptを使っている場合もあるからです。トランスパイラで良しなにしても、特にcssなんかはスルーしてしまうので怖いですよね。サポートしているブラウザのバージョンの下限と最新のバージョンでちゃんと動くことは確認したいですね・・・

サーバーにちゃんとデータをPOSTしてる?ちゃんとクエリパラメータ付与できてる?の確認

Jestでもできることですが、ブラウザ環境で、本当にちゃんと送れてるの? という観点はフロントエンドでの責務の範囲でテストをするべきだと思っています。ちゃんと本物のバックエンドサーバーからレスポンスが返ってくるかどうか、はE2Eテストレベルの領域だと思っています

で、ブラウザテスト最高!に行き着きました

フレームワークレスである(サードパーティなOSSに依存しない)

jestでuiライブラリ・フレームワークをレンダリングするには、基本的にはサードパーティが提供するossを使わなければなりません。reactは幸いにもreact-test-rendererという公式のtestレンダリングツールがありますが。他のフレームワークはどうでしょうか?なんだかんだtesting-libraryがほとんどの有名uiライブラリなどをサポートしてるので問題ないかと思いますが、更新頻度はそれぞれバラバラなはずです。そうなってくると新しいメジャーバージョンが上がっても、テストツール側が対応しなければ肝心の本体のバージョンを上げることが難しくなります。

そこでブラウザテストです!
ブラウザテストならこれらのフレームワーク、テストツールのバージョン互換もツールのありなしを考える必要はありません。超マイナーなフレームワークでもテストすることができます。無駄なことを考える必要はありませんね

ブラウザテストでのバグ -> 本番環境に限りなく近い

ブラウザ・バージョンごとの挙動・UIの確認ができるのがいいですよね。

window apiのモックなんて書かなくて済まなくなり、虚無モックを書く必要がなくなる

※後述するAPIのモックではレスポンスのモックコードは書かないといけないですが、虚無ではないはずです。

IntersectionObserverもResizeObserverも本物のAPIを使ってテストできます!最高!

オレノカンガエタ最強のブラウザテストの技術選定とテスト手法

技術選定

ブラウザテストツール: Playwright
https://playwright.dev/
safariやFirefoxなどの他のブラウザの対応をしてるのがGoodですね!あとこれ一本でvrtテストもできるしapiテストもできるテストが落ちた時の動画も取っておいてくれるので最高です。
※もしかしたらCypressでも同じことできたりするかもですが、筆者がガッツリ触ったのがPlaywrightくらい。というのでバイアスかなり掛かってます
nodejsだけでなくpythonやjavaでも書けるのも魅力的かもしれないです。

APIのモックツール: MSW
https://mswjs.io/
APIをモックする際に使います。ほんのちょびっとだけ工夫すればSSRでのレンダリングにも対応できるので最高です。ブラウザテストのフレーキーさの原因の一つにはAPIレスポンスの遅さ、とかもあったりするので、フロントエンドの責務の範囲に絞り込んだテストが簡単に書けるようになります。

(optional) playwright-testing-library
https://github.com/testing-library/playwright-testing-library
testing-libraryのようなdomクエリの書き心地でplaywrightのブラウザテストが書けます。
optionalと書いたのは、あまりメンテ頻度が多くない。ということと、playwrightがクエリを逆輸入してきたので将来性を考えたら導入しないほうがいいかもしれないかなと思ったからです。
https://playwright.dev/docs/locators#locating-elements

これくらいかな?少ないですよね!!これだけ?って思ったかもしれないですがこの2つ(or 3つ)で原理上ならほぼすべてのUIライブラリに対応できますね!!

テスト手法

  • 基本的なtesting-libraryで行うようなuiテスト

expect(screen.getByHoge("Fuga")).toBeVisible()
みたいなあれです

  • apiのクエリパラメータ・ヘッダー・POSTボディなどのスナップショットテスト
    送信時のAPIへの通信をinterceptできるのでいい感じに書けますよ!参考にzennですでに記事があるので(Jestでですが)参考にそちらを載せておきます!Playwrightでも同様に書けますし、Playwrightのほうが「本物のブラウザ」で送信しているという安心感があります

https://zenn.dev/takepepe/articles/frontend-webapi-snapshot-testing

  • VRTによるビジュアルリグレッションテスト
    こちらは個人ブログでPlaywrightとGithub Actionsで行えるようにした記事があるのでぜひ見に来てください!

https://u-yas.dev/posts/5yql4dzin

ブラウザテストの辛みポイント・・・

目を背けたいけどブラウザテストも万全じゃないですね

カバレッジが取れない(半分嘘)

正確にはChoromeブラウザで、かつIstanbulを使えばカバレッジがいい感じに取得できます。

https://playwright.dev/docs/api/class-coverage

ただIstanbulは公式ではbabel pluginしか提供されていないので、Next.jsと噛み合わせると

https://zenn.dev/terrierscript/articles/2022-12-26-playwright-next-js-coverage

パターン1

  • 本番環境ではSWC
  • テスト環境ではBabel

パターン2

  • 本番環境、テスト環境両方ともBabel

と2パターンのうちのどちらかの選択肢を取る必要があるかなと思っています。

Next.jsが今後どこまでBabelをサポートするのか考えると・・・

別の手段を考えるかコミュニティが作ってるswcでistanbulを再現するプラグインを使うしかないかなって思っています。

https://github.com/kwonoj/swc-plugin-coverage-instrument

異常系のテストがやりづらい

apiの場合はpathやクエリパラメータの設計によってレスポンスの出し分けができるので可能です。ただ正常系で発火しない例外コードをテストするのは相当難しいです。こういう一部分だけはJestでテストしてもいいかもしれないですね。

単体テストができない(最近できるようになってきてる)

今まではコンポーネント単位でのテストを行うのが難しかったのですが、CypressやPlaywrightでは徐々にコンポーネントテストができるようになってきています。成約は大きいですがそれでもすごいことです。

https://docs.cypress.io/guides/component-testing/overview

https://playwright.dev/docs/test-components

最後に、そしてポエム

最近あまりにもJestのテストが辛すぎて我慢できなくなり書きなぐってしまいました。ようは「The Testing Trophy」でテストを書くのが一番コスパいいよね?という話と & ブラウザテスト最高!
という2つの話がしたかったのです。

https://testingjavascript.com/

そして個人ブログで昔雑に書いた以下の2つの記事をちゃんと清書しておきたかったり

https://u-yas.dev/posts/xm7yw5tyyhqr

http://u-yas.dev/posts/owa2tisek5

自動テストをなんの目的で書いているのか、胸に手を当てて考えてみてください。虚無モックによるカバレッジ100%の神テストコードを書くのが目的ではないはずです。僕は自動テストはユーザーにバグを起こさず高品質なプロダクトを最速で提供していくためのテスト手法だと思っています。間違ってないはず。

JestのUIテストで虚無味を感じてしんどいと思って共感してくれた同士はぜひ❤かコメントか、Twitterで感想でも書いていただけると助かります。同士がいると、実感したい・・・

では!

~~ ポエム ~~

こういったライブラリ・ツールのツラミポイントを解消するには自分でその課題を解決するツールを書くか、その課題を解決しようとしているOSSに貢献するか、解決されるのを指を咥えて待っているか、かなと思っています。自分は今指を咥えてまつことしかできていないので、ここで自分で手を動かして解決できるようなITエンジニアになりたいなって。そう思ってます。すべてのOSSコントリビューターに感謝🙏

追記(2023/06/19)
この記事で取り上げているtakepepeさんの感想ツイートがいい感じにまとまってたので貼っておきます。100億%同意で、こういう視点で文を書けてなかったところが経験の差というか、文章力の差だなって痛感したので精進します!

Discussion

kuniwakkuniwak

はじめまして。

この記事を拝見していて私が最も気になったのは、実装者が最も気にするべきは単体テストにもかかわらずその言及がほとんどないことです。いわゆる Test Pyramid[1] ですね。この記事の冒頭でも単体テストと結合テストが混同されているという記述がありますが、それにしても困りごとの比重が Test Pyramid で理想とされているボリュームから外れていそうという印象を抱いています。外れていたら申し訳ないのですが、おそらく u-yas さんは結合レベルがとても高い状況で単体テストをやろうとしているんじゃないでしょうか。

結合レベルの高いテストフレームワークである Playwright や Cypress で単体テストをと言っているのがその推測の根拠です。この推測を裏付けるもう1つの点として UI テストの話の文脈で通信の intercept がでてくることがあげられます。通信によって変わるようなブラウザ上の揮発性の状態は UI 層からインターフェースによってモデル層に切り出すと、モデル層で結合レベルの低い、すなわちモックの少ない単体テストを実施できます。そしてもしそうできたなら代表的な状態ごとの表示を確認するごくわずかな UI 層のテストと、モデル層の状態遷移をみる単体テストに分割できます。するとモックが必要な UI 層のテストは数が少なくなるのでモックもさほど気にならない量になり、辛いところが緩和されるのではないかなと思ったのでした。

まとめると、実は u-yas さんの辛い状況の本質はWebアプリケーション本体の設計にあるのではないかと感じます。この点に比較すると実ブラウザか jsdom のどちらと結合するべきなのかという悩みは本質的ではないと感じました。

脚注
  1. テストレベルごとの理想的な量を表現しているのが Test Pyramid です。単体テストを最も多く、結合テストはそれより少なく、システムテストはそれよりさらに少なく、という量の関係が理想であるとしています。 ↩︎

u-yasu-yas

記事を読んでいただいて、また現状の自分の課題感がどこにあるのかを考えていただきありがとうございます!!

コメント読んだ自分の所感になります!最終的には

実は u-yas さんの辛い状況の本質はWebアプリケーション本体の設計にあるのではないかと感じます。この点に比較すると実ブラウザか jsdom のどちらと結合するべきなのかという悩みは本質的ではないと感じました。

これが問題の本質であることは疑いようもありません。そして問題の本質への対策であるモジュールの適切な分割方法も仰ってる通りの考え方で辛いモックを大量に書くということはなくなるかと思います。僕が知っている設計方法で思いつくものだとContainer/Presentationalパターンですかね。UIの責務とロジックの責務を分割してコンポーネントを正しく作れば虚無モックの辛さは解消されるのかと思っています。
https://zenn.dev/buyselltech/articles/9460c75b7cd8d1

ただちょっとだけ補足があります。

Test Pyramidについて

kuniwakさんが最初に紹介されているTest Pyramidですが、個人的にはこちらの考え方よりも、記事の最後の方で紹介した「The Testing Trophy」を目指す方が「コスパ」の観点では良いかなと思っています。
https://testingjavascript.com/

理想の姿としては Test Pyramidであることは私も同意見です。しかし現実としては時間的な成約だったり、設計をチームに浸透させるための諸々のコミュニケーションだったり、と超えなければならないハードル・課題があるのかなと思っています。

なので betterとして

  • 基本的なUIテストはブラウザテスト
  • 共通モジュール(コンポーネント・関数問わず)に関してはjest等による実装の詳細まで確認、境界値まで確認する厳格なテスト

がいいのではないかなと思っています。再度ですがあくまでbetterとしての考え方です。ベストはピラミッド型であることはものすごくわかります・・・

責務の分割を意識したコーディング頑張ってみます・・・!