フロントエンドのテストアーキテクチャ

に公開

近年 testing/library をはじめとしたフロントエンド向けのテストツールが普及し, フロントエンドでのテストが一般的になってきました.

また手動テストとE2Eテストを中心としたアイスクリームコーン型のテスト構成から, Testing Trophy の普及によってフロントエンドでも適切なテストを考える機会が増えてきています.

Testing Trophy

Testing Trophy
The Testing Trophy and Testing Classifications より

Testing Trophy は Kent C. Dodds が提唱したテスト比率に関する考え方です[1]. Testing Trophy はその名の通りのテスト比率に関する図が象徴的ですが, Testing Trophy が主張しているのは以下のようなことです(筆者の解釈です).

  • 実行コスト・時間は有限である
  • テストの信頼性を上げることが最重要
  • ユーザーの使い方に近いテストが信頼性が高い

このことから Testing Trophy では静的解析と統合テストを重視しています.

フロントエンドのテストにおける新たな課題

フロントエンドでテストが普及した一方で, 以下のような課題が出てきました[2][3].

  • スローテスト
  • 信頼不能テスト (flaky test)
  • 脆いテスト (fragile test)

Testing Trophy の問題点

Testing Trophy を忠実に守っていたとしても上述の課題は解決できません. その問題は統合テストを中心としたテスト構成にあります. そもそも統合テストとは一体なんでしょうか. フォームは統合テストでしょうか. それとも統合テストはページだけ? ページのテストを書けばページで使っているコンポーネントのテストは不要でしょうか.

そもそも flaky test や fragile test といった問題は単体テストや統合テストといった分類とは関係なく, モックや副作用によるものです. Testing Trophy ではこれらに対して効果的な指標をもたらしません.

テストアーキテクチャのためのテスト分類

ここまでで Testing Trophy だけでは上述の問題点を解決できないことがわかりました. そこで新たな考え方を導入しましょう. それが test sizes とテスト手法です[4][5].

Test sizes

Test sizes はテストが必要とするリソースによって分類したものです.

  • Small test: 実行プロセスは単一.
  • Medium test: 単一のマシン. 複数プロセス.
  • Large test: 複数のマシン. 複数プロセス.

Test sizes を図示したもの

『Googleのソフトウェアエンジニアリング』[6]より

リソースによって機械的に分類することでテストの区別に恣意性がなくなります. また信頼不能テストとなってしまう範囲をコントロールできます. Test sizes では small test を増やすことを目指します.

Feature Small Medium Large
Network access No localhost only Yes
Database No Yes Yes
File system access No Yes Yes
Use external systems No Discouraged Yes
Multiple threads No Yes Yes
Sleep statements No Yes Yes
System properties No Yes Yes
Time limit (seconds) 60 300 900+

3つのテスト手法

『単体テストの考え方/使い方』で紹介されているテスト手法の分類はテストの検証方法によって分類します.

出力値ベース・テスト: 戻り値を確認するテスト

出力値ベース・テストを表した図

状態ベース・テスト: 状態を確認するテスト

状態ベース・テストを表した図

コミュニケーションベース・テスト: オブジェクト間のやり取りを確認するテスト

ミュニケーションベース・テストを表した図

『単体テストの考え方/使い方』より

テストをこのように分類することで依存関係や変更容易性をコントロールすることができます. 例えばコミュニケーションテストでは必ずモックが必要です. そのため『単体テストの考え方/使い方』出力値ベース・テストが優れていると主張しています.

フロントエンドではE2EテストとUIコンポーネントに関するテスト, それ以外の関数に対するテストに分類することもできます. これらを出力値/状態/コミュニケーションベース・テストに割り当ててみましょう. 当然出力値ベース・テストとして検証できるのは関数のテストだけです. UIコンポーネントに関するテストの多くは状態を検証していると言えるでしょう. 外部依存性によりコミュニケーションベース・テストが必要になる場合もありそうです. E2Eテストも同様に状態ベース・テストとコミュニケーション・ベーステストとして検証されます. このことからフロントエンドにおける効果的なテストとは以下のように考えられます.

出力値 状態 コミュニケーション
関数 GOOD OK NG
UI - GOOD OK
E2E - GOOD OK

Test sizes とテスト手法

Test sizes とテスト手法からフロントエンドでは以下のように分類できます. UIコンポーネントに関するテストは可能な限り small test とし, 状態ベース・テストとして検証します. コミュニケーションベース・テストは避け, medium test で状態ベース・テストの方が望ましいでしょう. 出力値ベース・テストとなる関数に対するテストも積極的に採用しましょう. 関数に対するテストではテストダブルとなるような依存関係は避けましょう.

関数 UI E2E
Small GOOD GOOD -
Medium NG OK GOOD
Large NG NG OK

このようにテストを構成することで関数に対するテストでは速さと決定性を, UIコンポーネントに関するテストでは速さと忠実性のバランスを, E2Eテストでは忠実性を目的としてテストを作ることになります. 当然これらの分類は機械的に決まるため, テストの目的に対して共通認識を持ちながら実装することができます.

テストダブルについて

これまでの議論では単体テストでよく用いられる偽の実装:テストダブルについて言及しませんでした. ここではテストダブルを機能によって3つに分類します[7].

  • モック: テスト対象システムが利用し隠れた出力を担うオブジェクト
  • スタブ: テスト対象システムが利用し隠れた入力を担うオブジェクト
  • フェイク: 本物の実装同様に振る舞う軽量な実装

これらのテストダブルを使うことで以下のようなトレードオフが発生します.

  • モック:コミュニケーションベース・テストになる
  • スタブ:テストの忠実度が下がる(脆いテスト、偽陰性)
  • フェイク:テストの決定性が下がる(信頼不能テスト)

この記事では深く追求しませんが test sizes とテスト手法ではこれらのトレードオフを理解しながら実装することが求められます.

叫ぶテストアーキテクチャ

最上位レベルのディレクトリ構造と最上位レベルのパッケージのソースファイルは、「ヘルスケアシステム」「会計システム」「在庫管理システム」と叫んでいるだろうか?[8]

『Clean Architecture』第21章叫ぶアーキテクチャ扉絵

『Clean Architecture』より

これまででフロントエンドのテストに関する新しい分類を取り入れました. しかし分類をしただけでは, そのテスト構成が忠実に適用されるとは限りません. そこで明示的なアーキテクチャ, つまり叫ぶテストアーキテクチャとして構築します.

ここでは test sizes にしたがってテストに名前付けをしていくことにします. 関数やUIコンポーネント, E2Eテストは元々ファイルを分けているでしょうから, test sizes によって名前づけをすることで以下のような構成となります.

./
├── e2e
│   └── login
│       └── login.large.test.ts
└── app
    └── login
        ├── LoginForm.tsx
        ├── LoginForm.small.test.ts
        ├── LoginForm.medium.test.ts
        ├── page.tsx
        ├── validateUser.ts
        └── validateUser.small.test.ts

テスト実行も分離します. 例えば Vitest なら vite.small.config.ts として, test sizes ごとに設定ファイルを追加することで個別に実行することができます.

vite.small.config.ts
/// <reference types="vitest" />
import { defineConfig } from "vite";

export default defineConfig({
  test: {
    include: ["**/*.small.{test,spec}.?(c|m)[jt]s?(x)"],
  },
});
package.json
{
  "scripts": {
    "test": "vitest run --silent --config=vite.config.ts",
    "test:small": "vitest run --silent --config=vite.small.config.ts"
  }
}

このようにテストの決定性をコントロールできる test sizes に従ってテスト実行そのものを分離することで, テストが失敗する範囲を制限し隔離することになるため, 結果的に実行コストを下げることになります.


この記事ではフロントエンドにおけるテストに現れた課題が Testing Trophy では解決できないことを明らかにしました. そこで test sizes とテスト手法という新たな分類を導入することで, 機械的にテストを分類し, その分類ごとにテストの目的を絞ることにしました. さらに test sizes ごとにテストファイルを分けることで, テストの目的ごとにテスト実行を分けることができ, 柔軟なテスト実行計画にすることができます. このように明示的なテスト構成:叫ぶテストアーキテクチャを採用することで, テストの目的に合わせたより柔軟で効率的なテストを実行できます.

脚注
  1. The Testing Trophy and Testing Classifications ↩︎

  2. 第2回 偽陽性と偽陰性 ~自動テストの信頼性をむしばむ現象を理解する~ | gihyo.jp ↩︎

  3. 第8回 脆いテスト ~継続的な変更と改善を阻むテストの原因と対策~ | gihyo.jp ↩︎

  4. Google Testing Blog: Test Sizes ↩︎

  5. 単体テストの考え方/使い方 プロジェクトの持続可能な成長を実現するための戦略, Vladimir Khorikov, 須田 智之, マイナビ出版, 2022 http://book.mynavi.jp/ec/products/detail/id=134252 ↩︎

  6. Googleのソフトウェアエンジニアリング―持続可能なプログラミングを支える技術、文化、プロセス, Titus Winters, Tom Manshreck, Hyrum Wright, 竹辺 靖昭, 久富木 隆一, オライリー・ジャパン, 2021 https://www.oreilly.co.jp/books/9784873119656/ ↩︎

  7. 単体テストの始め方/作り方 ↩︎

  8. Clean Architecture 達人に学ぶソフトウェアの構造と設計, Robert C.Martin, 角 征典, 高木 正弘, ドワンゴ, 2018 https://asciidwango.jp/post/176293765750/clean-architecture ↩︎

Discussion