フロントエンド開発におけるテスト自動化について
はじめに
フロントエンド開発において、どこに/いつ/どうやってテストを書いたらよいのかまとめていきます なぜテストを書くのか?
予期せぬリグレッションを防止または検出する (あるコードを変更した際に、まったく関係ないと思われていた箇所でバグなどが生じてしまう可能性がある) テストコードがありその実行がCIなどで自動化されていることで、バグ修正や新機能の追加などにおいて既存のコードにバグを埋め込んでしまうリスクを軽減できる。また、リファクタリングや依存パッケージのアップデートなどのリスクも軽減できる。 また、上記のメリットにより心理的な負担の軽減も期待されます
テスト対象について
関数やクラス
その他
テストの種別/分類
テストには色々な分類の仕方があると思います
これらは人やチームによって分類や呼び方などが異なることも多いと思うため、あくまで参考程度に留めておくくらいで良いのかなと思います
実装した関数やクラスなどに対して他の要素とは独立してテストを行う
code:javascript
describe('sum', () => {
it('returns sum of numbers', () => {
expect(sum(1, 2, 3)).toBe(6);
});
it('should return zero if no arguments are given', () => {
expect(sum()).toBe(0);
});
});
SOLID原則などに従い、独立して効果的にユニットテスト可能な関数やクラスの比重を増やせると理想的ではあると思います ただし、実際に開発を行っているとなかなかそれが難しい場合も多いとは思う(例: テストコードがまだ存在ないプロジェクトにおいて、後から少しずつテストを追加するケースなど)ので、あまり拘りすぎず、まずはインテグレーションテストを中心に少しずつテストを増やしていくのも現実的なのではないかと思います
複数の要素(関数, クラス, ミドルウェア, 外部サービスなど)などを組み合わせてテストし、それらがきちんと意図した通りに連携されていることを確認します
フロントエンド開発においていくつか例を挙げると、以下のようなテストなどがインテグレーションテストと呼べるのではないかと思います 複数の関数やクラス、モジュールなどを組み合わせて、それらがきちんと連携されていることをテストする
自動テストがまだ整備されていないコードベースに対してテストを導入したい場合は、ユニットテストよりもまずはインテグレーションテストを優先して整備していくと効果的な場面も多いのではないかと思います
インテグレーションテストが増えてきたら、カバレッジなどを計測しつつ、ユニットテストがしやすい構造にコードを少しずつ置き換えていくと安全だと思います
もし置き換えていく段階でカバレッジが低下した場合は、どこかにバグなどが埋め込まれてしまっている可能性があるので注意するとよさそうです
APIサーバーを実際に起動してHTTPリクエストを送信し、レスポンスなどを検証する
一見、E2Eテストは万能な解決策のようにも見えますが、デメリットも多いので、適宜、他のテスト手法と組み合わせるのが良いと思います メリット
実際の本番アプリケーションと近い状況を再現してテストができるため、E2Eテストが通ればその機能に関してはきちんと動作していることが期待できる デメリット
メンテナンスや記述コストが高い
E2Eテストを用意したとしてもメンテナンスにコストがかかりすぎてしまい、形骸化してしまうリスクがある 実行に時間がかかる
インフラの構築や整備などのコストがかかる
ユニットテストやインテグレーションテストなどと比較すると、不安定になるがち (Flaky Test) テストコードが実装の詳細に強く依存していると、テストコードが意図せずして壊れてしまい、テストの信頼性が低下してしまう可能性があるためです 導入の方法としては、以下のようにすると効果的だと思います
1. バグが発生した際に、まずそのバグを再現するための失敗するテストコードを書きます。
2. 次にそのテストコードを実行してみて、実際にテストが失敗することを確認します。
3. 最後にその失敗しているテストコードがパスするようにバグを修正します。
こうすることで、テストコードの実行がCIなどで自動化されていれば、同じ原因でバグが再発することを防止できます ただし、実際には常にこの方法でテストをうまく書けるとは限らない場面も多いと思うため、必要に応じて後からテストを導入することなども検討するとよいと思います
また、このテストがあることにより、バグの再発が防止されます
テストダブルとはモック, スタブ, スパイ, フェイクなどのテストにおいて本物のオブジェクトの代替として機能するオブジェクトなどを指します どれを使うべきか?
フェイクとは、外から見た際にあたかも本物のオブジェクトであるかのように振る舞うオブジェクトのこと
フェイクを実装する際は、契約による設計における事前条件・事後条件・不変条件が本物のオブジェクトとフェイクとで一致していると理想的だと思います 例としては、バックエンド開発における永続化レイヤー(Repository, Data Access Objectなど)のインメモリ実装などがそれにあたります
上記の書籍では、フェイクに対してもテストコードを記述することが推奨されています
モック・スタブの乱用について
パッケージのスタブについて
個人的な意見としては、これらの機能は非常に便利ではありますが、どうしてもそれを使わないとテストが難しいというケースを除いて使用を避けるのがよいと考えています
これらの機能を使うと実装の詳細に強く依存した信頼性の低いテストに陥ってしまうというのが理由です (ホワイトボックステスト) 「テスト対象がどのパッケージに依存しているか?」というのは実装の詳細の中でもかなり詳細度の高いものに当たり、テストコードがこれらについての知識を持ってしまうことはあまり理想的ではないと考えます 理想としては、これらの機能を使わずとも単独でユニットテストが書けるようにすることや、mswなどの別の手段を用いてスタブを用意することが理想的ではないかと思います HTTPリクエストのスタブ
ユニットテストやインテグレーションテストを記述する際に、実際のAPIサーバーに対してHTTPリクエストを送信したくない、というケースは頻繁に発生するのではないかと思います
こういった目的を解決するために様々なパッケージが存在します
理由はfetch-mockやmoxiosなどは特定のHTTPクライアント(例: fetch, axios)と強く結合していることが挙げられます 例えば、moxiosを採用していた場合、もし「HTTPクライアントをaxiosからfetch()へ移行したい」というようなケースが生じた場合、テストコードの大幅な書き換えが発生する恐れがあります mswやnockなどのように特定のHTTPクライアントに依存しない方法であれば、そういったケースにおいても問題なく機能し続けてくれます 結合度などの観点からすると、この手法に従いスタブの使用をできるだけ抑えられると理想的ではあると思います 結合度の低下や凝集度を高めるのに役立つ手法について
こういったコードを書く方法についてはSOLID原則やその他の手法などについて学ぶと理解が深まるのではないかと思います どういった場面でテストを書くとよいか