「テストしやすさ」を設計時に確保する
「テストが書けない」は実装フェーズでは手遅れ
E2E テストが不安定、ユニットテストがモックだらけで何を検証しているか分からない——こうした問題の多くは、テストコードを書くタイミングではなく、設計の時点で決まっています。要素にセレクタが振られていない、現在時刻が直接埋め込まれている、外部 API 呼び出しがコンポーネントの奥深くに隠れている。これらは実装後に気づいても直しにくく、テストを書く側が無理やり回避策を積み上げることになります。
テスタビリティ(testability)は「あとで頑張る」ものではなく、設計フェーズで確保する属性です。ここでは、実務でよく効く3つの設計判断を具体的に見ていきます。
1. data-testid の命名規約を先に決める
CSS クラスやテキスト内容でテスト対象を特定すると、スタイル変更や文言変更のたびにテストが壊れます。かといって思いつきで data-testid を振ると、今度は命名がバラバラになり、どのテストがどの要素を見ているか追えなくなります。
実務で機能する命名規約はシンプルです。
{画面名}-{要素種別}-{識別子}の3階層で統一する(例:bug-report-form-submit-button)- 一覧の行やカードなど繰り返し要素には、末尾に一意な ID を付ける(例:
bug-report-row-{id}) - 状態によって出し分けるモーダルやトーストは、状態名を含める(例:
save-toast-error/save-toast-success)
これを実装前に決めておけば、テストを書く人はコードを読まずにセレクタを推測できます。
2. 時刻とランダムを注入可能にする
「有効期限が切れた」「1時間以内に3回リトライしたら制限」のようなロジックを new Date() や Math.random() の直接呼び出しで書くと、テストは実行するたびに結果が変わるか、実行時刻に依存して失敗します。
設計段階での対処は難しくありません。時刻とランダムの生成源を関数の外から渡せるようにするだけです。
// 避けたい書き方
function isExpired(token) {
return token.expiresAt < Date.now();
}
// テスト可能な書き方
function isExpired(token, now = Date.now()) {
return token.expiresAt < now;
}デフォルト引数にしておけば本番コードの呼び出し側は変更不要で、テストだけが固定値を渡せます。Rails 側でも Time.current を直接埋め込まず、Freeze 可能な形にしておくと同じ恩恵があります。
3. 外部 API のモック境界を1箇所に決める
外部 API 呼び出しをコンポーネントやサービスクラスの各所に散らばせると、テストごとに異なる箇所でモックを差し込むことになり、モック漏れによる「本物の API を叩いてしまうテスト」が発生します。
境界を1つのアダプター層に集約するのが定石です。
- 外部通信は必ず専用クラス・モジュール(例:
GithubIssueClient)経由にする - ビジネスロジック側はこのアダプターのインターフェースにのみ依存する
- テストではこのアダプター1箇所だけを差し替えればよい状態にする
これにより、モックすべき境界がコードベース全体で1箇所に固定され、新しく参加したメンバーでも「どこをモックすればいいか」に迷わなくなります。
設計レビューで確認する3項目
新しい画面・機能の設計レビューでは、次の3点をチェックリストとして持っておくと実装後の手戻りを防げます。
- 操作対象になる主要な要素に
data-testidの割り当て方針があるか - 時刻・乱数に依存するロジックが外部から注入可能になっているか
- 外部 API 呼び出しが1つのアダプター層を経由しているか
いずれも実装コストはわずかですが、あとから入れ直すのは既存コードの広範囲な書き換えを伴います。設計レビューの段階で確認する価値があります。
Bugoon での実践
Bugoon はウィジェットで対象要素をスクリーンショット付きでアノテーションし、操作ステップを記録した上で GitHub Issue として起票します。バグ報告に写る要素が明確な data-testid を持っていれば、修正後にその要素を対象としたテストを追加する作業もスムーズになります。報告と再発防止のテストが同じ要素を指し示せる状態を、設計段階の命名規約が支えている形です。