私はAndroidでエミュレーター無しでスクショテストができる Roborazzi というライブラリを作っていて、もう少し周辺について詳しくなっておこうと思いまして、周辺技術のベストプラクティスを薄く広く軽く調べて見ます。
Jestを使ったスナップショットテスト:
テストの流れが以下のように書いてあります。
- 実行して、画面を描画する
- Captureする
- 保存する
- もう一度実行する
- 比較する
- 画像をアップデートする
比較のところではdiffや比較の画像を出したりする。
以下のtoMatchSnapshot()で、なければ保存したり、あれば比較したりする。
test(‘should match snapshot’, () =>
{
const data = {name: ‘John’,age: 30,email: ‘john@example.com‘};
expect(data).toMatchSnapshot();
});
Jestにはinteractive modeがあって、テストが変更画像にたどり着いたときに画像を受け入れる、消す、スキップする、やめるからアクションを選べたりする。
ベストプラクティス
- 意味があり、記述的なテストの名前を利用する。何がテストされているのかをはっきりさせる。テストの目的をはっきりさせ、失敗の原因を特定しやすくする。
- 依存を切り離す。外部の要因や依存により影響を受けないようにする。テスト環境をコントロールできるようにし、Snapshotを安定させる。
- 不要なスナップショットは避ける。価値がある場所のみにする。すべての場所では取らず、クリティカルで複雑な場所を取る。
- 定期的なレビューとアップデート。古い画像は偽陽性(false positive)を引き起こす。必要に応じて、画像を見直す機会を作る。
- スナップショットの汚染を避ける。日付データなどで変更されるのを防ぐ。ノイズになるため。
- Diffを注意深く観察する。意図的な変更か、リグレッションかを見分ける。
- Inline Snapshotsを動的なコンテンツには使う。Inline snapshotはテストケース内で、snapshotを撮るやつのこと。
- Snapshot Serialisationを使う。React, Redux, or GraphQLなどそれぞれに対して、適切なSerializerを使う。
- Snapshotをバージョンコントロールする。チームが確認できるようにして、レビューできるようにする。いつなぜSnapshotが更新されたかがわかるようになる。デバッグが楽になり、テストの安定性を増やす。
- 通常のテストのテクニックを利用する。スナップショットテストは補うものであり、unit tests, integration tests, and manual testingを交換するものではない。両方利用していくべき。
他のツール
- React Testing Library:
toMatchSnapshot()
を提供。実装詳細ではなく、user interactionsとcomponent behaviourをテストすることを重視している。 - Enzyme:
toJSON()
を提供。コンポーネントの探索と変更に特化したアプローチを取っている。React Testing Library
のかわりや一緒に使える。 - React-test-renderer:
React’s official testing utilities
の一つ。JSONのような形でレンダリングできる。 - Snapshot Diff: clearer and more readable diffを出す。なので、失敗が理解しやすい。
- Storybook: 有名なコンポーネントライブラリを作るためのライブラリ。Storyshotのようなアドオンを通じて、スナップショットを取る。
メリット
- テストメンテナンスの簡略化: アサーションの書き換えがいらない。
- 読みやすく簡潔なテストになる: ボイラープレートとなるコードがいらない。メンテナンスがしやすい。
- フィードバックループが速い: 一度画像を作るともう一度前のバージョンで画像を作らなくてよいため。
- 表示の変更の検知: 一貫性のある表示を保てる。
- ドキュメンテーションとして利用できる: 新しいメンバーのオンボードに利用したり、コードレビューを効率化する。
- プラットフォームの問題を検出できる: プラットフォームから独立しているので、CI/CDなどの環境によらずに実行できる
デメリット
- スナップショット自体はinputのデータやユーザーのインタラクションや条件が表示されない。これはスナップショットのみでの比較を難しくする
- 偽陽性、偽陰性: スナップショットの変更していないときなどに起こる。レビューやメンテナンスが必要になる。
- 壊れやすいテスト: 日付やランダム値を使うテストは頻繁にアップデートが必要になる。そのようなコンポーネントは常にテストを壊し生産性を下げる
- 意味の理解の欠如: 微妙なロジックのエラーや間違った挙動を許容してしまい得る。ユニットテストやインテグレーションテストと一緒に使って正しさとエッジケースをカバーすべし。
- サイズとパフォーマンス: 大きくなるとリソースを消費する。また大きいスナップショットはdiffを確認するのも難しくなる
ユニットテストとの違い:
ユニットテストはコードのユニットの実装、スナップショットテストはアウトプットと構造にフォーカス。両方良い。
Visual Regression Testing:とスナップショットテスト:
スコープ: スナップショットテストは関数単位。Visual Regression Testingは画面単位など。
目的: スナップショットテストは内容と構造をチェック。Visual Regression Testingは表示の確認。
技術: スナップショットはアウトプットをキャプチャする。スクショや画像を比較する。
ツール: スナップショットはJestとかについてて、Visual Regression TestingはSeleniumとかを使う。
(正直良く分かってなかった。)
How to Test Your Apps using Jest, Testing Library, Cypress, and Supertest
Cypressによるテストはユーザーの環境に近い。
タイムトラベル機能があり、テストにカーソルを合わせるとそこの画像が出せる。
ユニットやインテグレーションテストでも使えるしE2Eでも使える。
SupertestはHTTP requestsをシミュレートする。バックエンドのアプリとの連携を楽にする。
こんな感じ。
// About.test.js
describe('AboutPage', () => {
it('Renders correctly', () => {
cy.visit('http://localhost:3000/about')
cy.contains("I'm the about page!")
})
it('switch btn toggles text', () => {
cy.contains("It's on!")
cy.get('.switchBtn').click()
cy.contains("It's rolling!")
cy.get('.switchBtn').click()
cy.contains("It's on!")
})
https://www.freecodecamp.org/news/test-a-react-app-with-jest-testing-library-and-cypress/ より
(一つのテストに色々入ってくるのが面白い)
ちょっとcypress興味出てきたので公式ドキュメントのベストプラクティスを読む
Cypress Best Practices
- テストの構成、ログイン、ステートのコントロール
アンチパターン: ページオブジェクトを共有して、ショートカットを利用せず、それぞれのページでログイン処理をする。
ベストプラクティス: テストが独立しており、ログインはコードで行われ、アプリケーションの状態のコントロールを行う。
AssertJS - Cypress Best Practices に詳細があるそう。
- 要素の選択
アンチパターン: 変わるような壊れやすいセレクタを使わないで。 (ボタンタグでの指定とか)
ベストプラクティス: data-*属性を提供して、CSSやJSの変更から独立させる。
- 返り値を保存しないようにしよう
これはcypress特有の話っぽいので、スキップ
- 外部サイトへのアクセスを避けよう
避けよう
- 前のテストに依存しないようにしよう
依存しないようにしようという話ではあって、一個前のブログだとこれをやってましたね
beforeEachで共通の処理をする、これが理想的な形っぽいです。
describe('my form', () => {
beforeEach(() => {
cy.visit('/users/new')
cy.get('[data-testid="first-name"]').type('Johnny')
cy.get('[data-testid="last-name"]').type('Appleseed')
})
it('displays form validation', () => {
// clear out first name
cy.get('[data-testid="first-name"]').clear()
cy.get('form').submit()
cy.get('[data-testid="errors"]').should('contain', 'First name is required')
})
it('can submit a valid form', () => {
cy.get('form').submit()
})
})
- E2Eで一つのアサーションだけの小さいテストを書かないようにしよう
アンチパターン: ユニットテストみたいに書くこと
ベストプラクティス: 複数のアサーションを書く。あんまり気にしないで。
パフォーマンスの問題。
- after Or afterEach Hooksを使わないようにしよう
アンチパターン: after Or afterEach Hooksを使うこと
(これもCypress特有の話っぽいです)Cypress commandを使ってデバッグしたりが難しくなるため。beforeTestとかで初期化しようという話をしている。
- Unnecessary Waiting
wait(5000)とかはいらないという話。
- 賢くテストを走らせよう
並列化、ロードバランシング、キャンセルの自動化、テストの優先度付けなどのコンビネーションがcypress cloudで使えるそう
-
Webサーバーの起動はCypress scriptsが始まる前にしよう
-
baseUrlは全体で設定しよう
pointfreeco/swift-snapshot-testing
画像を作れるだけじゃなくて、recursiveDescriptionを指定して、詳細を取得したりなどができる。
assertSnapshot(of: vc, as: .image(on: .iPhoneSe))
assertSnapshot(of: vc, as: .recursiveDescription(on: .iPhoneSe))
Snapshot strategyを使うことで、なんの値に対してもテストが書ける。自分で戦略を書ける。
Opinions on snapshot testing
みんな何も見ずにSnapshotをアップデートし始めて、何にも役に立たなかったよという意見が載っている。
確かに文字列を人間が確認するのは難しいのかもという。
Snapshot Testing on iOS
Shopifyのエンジニアのブログ。
PRのスクショの比較のようなものが出せるのが良いと言っている。
正しくものを作っているか?正しいものを作っているか?の間ぐらいの位置にあるという位置づけだそう。
Con: It’s easy to overdo snapshot tests, feel confident in your app’s correctness, but not actually be testing the correct thing.
自信は持てるけど、正しくテストできていないことがある。
これにつきますね。
個人的なまとめ
- Snapshotテストとスクリーンショットテスト(Visual Regression Test)は違うもの。
- 比較が分かりやすくできるようにしつつ、入力内容や期待内容などを分かりやすくしないと崩壊する場合がありそう。
- 結局Snapshotやスクリーンショットだけでは正しさが分からないので、Unit TestやIntegration Testなどといった他のテストを組み合わせるのは良いのかなという感じ。