はじめに
開発をしていて「テスト書いていますか?」と聞かれることは多いと思いますが、
どこから手をつけ? どの粒度で書くか? どういう順序で? と聞かれたときにすぐに答えられる人は多くないと思います。
場当たり的にテストを書き始めると、本来の「バグを防ぐ」「リファクタリングを支える」といった目的を果たせなくなってしまいます。そのためにまず、「テストコード戦略」 とは何か、なぜ必要なのか、そして何について考えるべきかを整理してみようと思います。
テスト戦略とは?
テスト戦略とはひとことで言えば、
「開発対象に対して、どこに、どのレイヤーで、どんなテストを書いて、どう管理していくかをあらかじめ決めておくこと」
では、どうして予めこのような方針を決定しておく必要があるのか?
なぜテスト戦略が必要なのか?
1. 品質とコストのバランスをとるため
すべてのコードに対して、テストコードを書くのは現実的ではないと思います。そのため、重要度や変更頻度を考慮してあらかじめ「どこまで作成するべきか」を考えておく必要があります。
2. テストの破綻を防ぐため
テスト戦略を決めていないテストコードは各々の裁量によって、書かれていくと思います。
そのようなコードは変更があった際に即座に対応できず、リファクタリングの際にも課題になってしまいます。
戦略として、「どこをブラックボックスとみなすか」「どこに依存しないか」を定めることで、保守性の高いテストが書けます。
3. チームの開発効率を上げるため
「この機能のテストはこのレイヤーでやる」といった共通理解があるだけで、認識合わせのコストが激減します。レビューもしやすくなりますし、レビュー基準も明確になります。
考えておくメリット
・品質の向上、バグの早期発見と修正
・変更に強いコードの実現、リファクタリングや新機能追加時に手戻りを防ぐ
・ドキュメントとしての機能、テストコードを見ることで仕様の理解の助けとなる
・チームの標準化
考えていない場合のデメリット
・テストの質・粒度にばらつきが出る
・カバレッジの過不足(過剰 or 不足)
・メンテできないテストが積もる
・バグが早期に検出できなくなる
テスト戦略は何を考える必要があるのか
下記らへんを予め定めておく必要がありそうです。
結構、ボリューミー
項目 | 説明 |
---|---|
対象範囲の定義 | どのコードをユニットテスト対象とするか(例:ビジネスロジック、ユーティリティなど) |
テスト設計方針 | 正常系・異常系・境界値など、どういう観点でテストを書くか |
フレームワーク選定 | Jest、JUnit、pytest、PHPUnit、Kotest など、何を使うか |
命名規則・構成ルール | テスト関数名やフォルダ構成のルール |
カバレッジ目標 | 何%を目指すか、例外的に不要と判断できる部分 |
責任分担 | 誰が書き、誰がレビューし、誰が保守するか |
レビュー方針 | テストコードもレビューポリシーの対象とするか |
テスト作成の優先度・順番
すべてのコードに対して、テストコードを作成することは前述のとおり現実的ではないので、私の場合は下記のような観点で優先度や順番を決めて作成するかどうかを決めています。
観点 | 説明 | 具体例 |
---|---|---|
ビジネス上重要な処理 | 売上や業務フローに直接関わる処理 | ユーザーの会員登録 ログイン処理 注文の確定処理 決済処理(カード決済、ポイント使用など) |
バグが起きやすいロジック | 複雑な分岐や演算、条件が絡むようなロジック密度の高い処理 | 複雑な割引計算処理 勤怠集計やシフト自動計算 バリデーション処理 |
変更頻度が高い箇所 | プロダクト開発の中でよく改修・追加要望が入る処理 | 商品検索・フィルター処理 ユーザー登録/編集画面 管理画面での表示項目変更ロジック ステータス判定(例:未処理・対応中・完了のステータス変化) |
外部連携がある機能 | テスト環境によって挙動が変わる可能性があるため、疎結合な構造とスタブ・モックの導入が必要 | メール通知
データベースへの登録・検索 外部APIとの連携(例:決済、天気情報など) ファイルアップロード処理(S3連携など) |
スタブ・モックとはそもそもなんなのか
スタブとモックがそもそも何なのか、どのような場面で、どのような目的で、違いは何なのか改めてまとめておきます。
目的 | テスト対象との関係 | 具体的な違い | |
---|---|---|---|
スタブ(Stub) | 外部依存の戻り値を固定して返す | 外部依存の解消 | テスト対象の「動作を成立させるため」に使う。主に「値を返す」役割。 →結果だけを見る |
モック(Mock) | 呼び出し回数や引数などの検証をする | 「本当に正しく使われたかを確認する」 | テスト対象の「動作が正しかったかを検証する」ために使う。呼び出し回数や引数チェックなど「振る舞いの検証」に使う。 →処理の中身をみたい |
スタブ・モックの利用方針(レイヤー別)
ドメイン層
テスト対象:純粋な関数
外部依存の例:基本的に依存なし
利用方針:モック・スタブは不要
ユースケース層
テスト対象:ドメイン+I/O操作
外部依存の例:リポジトリ、通知サービスなど
利用方針:リポジトリはスタブを、通知系はモックを利用
インフラ層
テスト対象:外部I/O処理の実装
外部依存の例:DB、メール、S3、APIなど
利用方針:スタブで外部依存(DB、APIなど)の挙動を模倣し、副作用のある処理を置き換える。
具体的な使い分け例
kotlinでのメールアドレスの変更時の通知を例に使用しています。
スタブ
DBへのアクセスを模倣し、固定のユーザーを返すだけなのでスタブ
class StubUserRepository : UserRepository {
private val dummyUser = User(id = UserId("123"), email = "old@example.com")
override fun findById(id: UserId): User? {
// 固定値を返すことで、DBアクセスをスキップ
return dummyUser
}
}
val repository = StubUserRepository()
val user = repository.findById(UserId("123"))
モック
呼び出しと内容の妥当性を検証したいので モック
val mockMailer = mock<Mailer>() // Mock作成
// 実行
notifier.notifyChange(user, oldEmail)
// 検証
verify(mockMailer).send(
to = oldEmail,
subject = "Your email has been changed",
body = contains(user.email)
)
まとめ
「とりあえずテストコードを書き始めてみよう」では、属人化と品質劣化は避けられない気がするので、個人開発でない限りはチームで以下のようなルールを明文化しておくほうがいいです。
・どの層にどういうテストを書くか
・どの場面でモック・スタブを使うか
・テストの優先順位や対象範囲の考え方
特にスタブ・モックの使い分けは、「テストで何を検証したいのか」 という目的に紐づいて判断する必要があるかと思います。