フロントエンドテストについて考えてみた
テストを書く理由
テストコードを書く時間があれば機能を実装したい、と思うかもしれません。
テストコードを書かないことは短期的には時間の短縮になりますが、中長期的に見ると時間の短縮にならないと言われています。特に複雑なロジックや隠れた仕様についてはモンキーテストでカバーできないことが多くバグが表面化されないケースもあります。不具合の発見が遅れれば遅れるほど傷は深くなります。
テストを書くことは大変です。意味のあるテストを書いていくためには、皆が信頼できるテストだと認識していることが大事です。
参考:https://speakerdeck.com/twada/building-automated-test-culture-2022-autumn-edition
t_wadaさんの「テストを書くことで品質がわかるようになる、わかることこそ大事」「テストを書くだけでは品質は良くならない」というお言葉があります。テストを書いた後の設計と実装が大事です。
テストの種類
上図はTesting Trophyと呼ばれるフロントエンドのアプリケーションにおけるテストを書くための考え方です。
上にいくほどユーザーに近くなるので品質保証性は高くなりますが壊れやすく実行速度は遅いです。下はユーザーから遠い部分を保証するので品質保証性は低いですが壊れにくく実行速度が速いことが特徴です。TestingTrophyの体積が大きいテストほどテストを書くべきと言われていますが、どのくらいの量をテストするかはプロジェクトごとにバランスを見ると良いです。
上位のテストが十分に高速でメンテナブルであれば低レベルのテストは省いても良いと言われています。
参考:Learn the smart, efficient way to test any JavaScript application.
(他にも Testing Pyramid, Software testing ice cream cone などの考え方があります。)
End to End
UIテスト
Playwright, Cypress(ブラウザ環境でのUIテスト)
Chromatic(Storybookを用いたUIテスト)
Autify, mabl(ノーコードUIテストツール)
シナリオベースで画面操作を想定した結合テストを書くことが多い。
Integration
結合テスト
componentsやhooksなど複数の対象を合わせたテスト。
TestingLibrary, Jest
Unit
単体テスト
TestingLibrary, Jest
Static
静的なテスト
リンターや型システムを利用
ESLint, TypeScript
コードカバレッジ
テスト対象のコード全体のうち、テストコードによってカバーされている部分の割合を解析して表示されます。
実際にはESLintやTypeScriptでカバーできるものもあるので、必ずしもカバレッジ100%にこだわる必要はないと言われています。カバレッジはテストの作成時間とトレードオフの関係です。
参考:Front-end Testing Strategy
npm run jest --coverage することで計測することができます。
Stmts(ステートメントカバレッジ)
テストで実行された実行可能命令(ステートメント)の割合
Branch(ブランチカバレッジ)
テスト対象コードに存在する条件分岐(ブランチ)の網羅率
Funcs(関数カバレッジ)
すべての関数のうちテストで最低1回実行された関数の割合
Lines(行カバレッジ)
テストで実行された行の割合
基本的な書き方(Unit Test)
テスト対象
// num.ts
export const sum = (a: number, b: number) => a + b;
テストファイル
// num.spec.ts
import { sum } from './num'
// module名の記述
describe('sum', () => {
beforeAll(() => {
// テストの事前処理
// テストごと実行する場合はbeforeEach
})
afterAll(() => {
// テストの後処理
// テストごと実行する場合はafterEach
})
// テストケースの記述
test('xxxにセットするとxxxxになること', () => {
// テスト対象の実行
// 結果のアサーション
expect(sum(1, 2)).toBe(3)
})
})
振る舞い駆動開発(Behavior-Driven Development / BDD)ではユニットテストは振る舞いを記述するものと言われています。サポートするAPIとしてexpectが実装され、仕様書に近い書き心地になっています。
ちなみに、BDDはTDD(テスト駆動開発)から派生されたものです。TDDでは最初に失敗するテストを書いて次にそのテストが成功する実装を書く手法です。TDDを体験したい場合は Rails Tutorial が親切なイメージがあります。(jsは知らないので知ってる人教えてください)
ユニットテストにおいて基本的には、テスト対象外の仕様を把握しなくて良いコードが良しとされておりテスト対象が他moduleをimportしている場合はテストダブルすることが多いです。
jest公式ではspy, stub, mockなど厳密に分けた定義をしておらず、テストダブルをすべてモックと呼んでいます。
jest.spyOn()
モックを生成
jest.fn()
オリジナルな実装のないモックを生成
jest.mock()
moduleに対してモックを生成
実際にテストを書く際にはwatchモードが提供されており --watch することでテスト再実行のコマンドを打つ必要がなくなるので楽です。
Q&A
private methodsはテストしなくて良いのか?
直接テストするためにexportする必要はなくpublic methodsから呼ばれていれば間接的にテストすることができるので大丈夫です
server側のテストとブラウザ側のテストを分けるにはどうしたら良いのか?
ブラウザ環境を想定したテストを実装するときはtestEnvironmentをjsdomにする必要があります。デフォルトの設定を変えたい場合はjest.configから変更できます。
export default {
"testEnvironment": "jsdom",
}
テストファイルごとに変更したい場合は、ファイルの先頭にコメントをつけることで変更できます
/**
* @jest-environment node
*/
参考: https://jestjs.io/ja/docs/configuration#testenvironment-string
jsdomに実装されていないメソッドをモックする方法もあります。
参考:https://jestjs.io/ja/docs/26.x/manual-mocks#jsdom-に実装されていないメソッドのモック
どうやってデバッグするのか?
単純にテスト対象のファイルやテストコードに console.log することでデバッグ可能です。testing-libraryだと screen.debug() や logRoles が用意されています。
vscodeでは vscode-jestが提供されています。