173.1K Views
June 07, 25
スライド概要
JJUG CCC 2025 Spring 登壇資料
著書『アーキテクトの教科書 価値を生むソフトウェアのアーキテクチャ構築』(翔泳社)
ユニットテスト 基礎講座 Jun. 7, 2025 @JJUG CCC 2025 Spring Takeshi Yonekubo
About Me 米久保 剛 (よねくぼ たけし) ITアーキテクト 『アーキテクトの教科書』(翔泳社) X: @tyonekubo note: https://note.com/yonekubo
ユニットテスト、書いていますか?
ユニットテスト、十分に書けていますか? ユニットテスト、上手に書けていますか?
ユニットテストについての課題認識 • ユニットテストを書くことは当たり前になってきた • 書籍など(日本語の)情報源は多くない • ベストプラクティスは浸透しておらず、誤った認識も 多い
「テストコードなんて、 AIに書かせればよくね?」
事実、AIにテストコードは書ける。 (今回のサンプルコードの大半は AIエージェントに書かせた)
だが、AIにテストコード作成を 任せっきりにすべきではない
理由❶ AIが生成するテストは平均点 プロダクションコード 仕様書 (Markdown) AIエージェント LLM 「テストコード書いて」 テストコード 学習済みの一般知識+コンテキ ストに含まれる固有知識をもと に、確率的にもっともらしいテ ストコードを生成 AIは状況に応じたテスト戦略は考えてくれない (人間の指示が必要)
理由❷ AIは間違える プロダクションコード 仕様書 (Markdown) AIエージェント LLM 「テストコード書いて」 テストコード テスト条件や検証内容の妥当性は、 100%の精度にはなり得ない プロダクションコードの誤りはテストで検知し修 正できるが、テストコード自体を守るものはない
現状は、人間がテストの戦略を立て、 テストの妥当性を評価しなければならない
テストコードはVibe Codingするべからず 雰囲気
Automated Testing(自動テスト) に関するドメイン知識は重要さを増す
Part 1. ユニットテストの基本概念
ユニットテストとは ユニットテストと統合テストの境界は曖昧 (解釈による) E2E Test Integration Test Unit Test
テストサイズによる分類 Googleでは、共通理解促進のため、テストサイズ (S/M/L)による分類が用いられる 出典: “Test Sizes” (Google Testing Blog) https://testing.googleblog.com/2010/12/test-sizes.html Large Medium Small
ユニットテストの定義 • 「単体(unit)」と呼ばれる少量のコードを検証するこ と • 実行時間が短いこと • 隔離された状態で実行されること
ただし、古典学派とロンドン学派に よって「ユニット」の捉え方が異なる ※古典学派とロンドン学派のスタンスの違いや、 歴史的経緯等は『テスト駆動開発』付録Cを参照
ユニットテストの定義(古典学派) • 1単位の振る舞い(a unit of Behavior) を検証すること • 実行時間が短いこと • 他のテスト・ケースから 隔離された状態で実行されること
ユニットテストの目的 主要な目的: 1. 期待どおりに正しく動作することを検証する ✓バグを摘出する 2. 退行を防ぐ(回帰テスト) ✓ミスによる機能退行を検知することができる 3. ドキュメンテーション ✓テストコードという実例を通して仕様を理解できる (Specification by Examples)
質の良いテストが必要 単にテストを作成すれば十分ということではありません …作成されたテストの質が悪ければ、テストを全くしな い場合と同じ結果になる 出典: “単体テストの考え方/使い方” 第1章
テストコードは散らかりやすい 具体値で記述するテストコードは、プロダクション コードの数倍の規模となる →放っておくと散らかっていく 出典: “アーキテクトの教科書” 第5章
テストコードのSOS テストコードは意識的にきれいな状態に保ち、 負債化するのを防ぐ ✓構造化されている(Structured) ✓整理されている(Organized) ✓自己文書化されている(Self-documenting)
小まとめ •ユニットテストの「ユニット」が指すもの を明確にせよ(=1単位の振る舞い) •テストを作成するだけでは不十分、質の良 いテストを作成せよ •テストコードは散らかりやすいので、意識 してきれいな状態を保つべし
Part 2. テスト対象の振る舞いの識別 ー あるいは設計という行為
サンプルアプリ仕様:映画チケット料金計算 通常料金 大人 2,000円 高校生 1,000円 シニア 1,500円 小中高生 1,000円 大学生 1,500円 幼児 1,000円 割引料金 • • • • 水曜日割引:1,300円 ファーストデー(毎月1日):1,300円 映画の日(12月1日):1,000円 会員割引(月〜木):1,300円 クーポン料金 • 提携先サービス毎に適用条件と料金が異なる ※料金が同額の場合は通常>割引>クーポンの適用順 サンプルコード: https://github.com/yonetty/cinema-ticket-calc
“1単位の振る舞い” を識別する
トランザクションスクリプト if文などを駆使して書かれた一枚岩のロジック 「レガシーっぽい コード書いて」 「ほい つ」 LegacyPriceCaluculationService.java
対応するテスト 6カテゴリ 34ケース テストケースの例 「ユニットテスト も書いて」 「ほい つ」 高カバレッジ
Q. テストの網羅性は十分か?
(QAエンジニア) 「料金計算ロジックの因子水準数より、 全網羅だと384ケース、ペアワイズ法で 2因子網羅だと49ケースなので、34ケース は若干少なくないでしょうか?」
(開発者) (実装上は相関のない因子もあるし、 開発者テストとしては十分な気もするが..)
振る舞いが大き過ぎる! トランザクションスクリプトはユニットテストに不向き (このコードでは 断言できないな…)
Divide and Concur 分割して統治せよ
振る舞いを分割する 大きな振る舞いを、複数の小さな振る舞いに分割する 通常料金を取得する 適用可能な最安の割引を判定し、 クーポン適用料金がある場合は比 較してより安い料金を返す。 どちらもない場合は顧客分類に対 応する通常料金を返す。 適用可能な割引の中から 最安の料金を判定する Too big! 通常料金、割引料金、クープン適用 料金のうち最安の料金を判定する
処理フローロジックと中核ロジックに分ける アプリケーションサービスは、処理の流れの制御に徹する 通常料金を取得する 料金計算 適用可能な割引の中から 最安の料金を判定する アプリケーション 通常料金、割引料金、クープン適用 料金のうち最安の料金を判定する サービス 個々のビジネスロジック
関心の分離 個々の小さな振る舞いに入力される因子数は、少なくなる 顧客分類 顧客分類 日付 通常料金を取得する 日付 会員フラグ クーポン料金 会員フラグ 料金計算 適用可能な割引の中から 最安の料金を判定する (通常料金) (割引料金) クーポン料金 通常料金、割引料金、クープン適用 料金のうち最安の料金を判定する
用語説明:因子水準、組み合わせテスト 組み合わせテストにおいて、 パラメーターとなるものが「因子」 パラメーターの取る値の種類が「水準」 因子 水準1 水準2 水準3 Javaバージョン Java 17 Java 21 Java 24 ディストリビューション Oracle Corretto Zulu OS Windows macOS Ubuntu 水準4 CentOS 全網羅:すべての因子水準の組み合わせをテスト 上記例だと、3 x 3 x 4 = 36通り 2因子網羅:任意の2因子において全組み合わせが担保されるようテスト 上記例だと、12〜16通り程度に削減可能(方法:直交表、ペアワイズ法など)
テスト設計のしやすさ 因子数が減れば、テスト条件(組み合わせ数)も減る 通常料金を取得する 適用可能な割引の中から 最安の料金を判定する 顧客分類 日付 会員フラグ 一般 12月1日 会員 シニア 12月2日 非会員 大学生 11月30日 中高生 11月1日 小学生 水曜日 幼児 月-木 金土日 通常料金、割引料金、クープン適用 料金のうち最安の料金を判定する (通常料金) なし (割引料金) クーポン料金 ※実際には、 月・日・曜日に 因数分解した方が良い X円 Y円 なし Z円 ※実際には、X・Y・Zの大小関係という 因子もあるので組み合わせは増える
小まとめ •ユニットテストで検証すべき「1単位の振る 舞い」を識別せよ •大きなものは、分割して統治せよ •小さくすることで、テスト設計の容易性も 向上する
Part 3. 質の良いテストコードを書くには
テストコードのSOS “構造化されている(Structured)”
パッケージ構造 プロダクションコードもテストコードも、水平分割(技術観 点)ではなく垂直分割(業務観点)でパッケージを設計する com └── example ├── domain │ ├── coupon │ └── price ├── persistence │ ├── coupon │ └── price └── web ├── coupon └── price com └── example ├── coupon │ ├── domain │ ├── persistence │ └── web └── price ├── domain ├── persistence └── web 料金計算の改修時は、この部分の テストスイートのみに集中できる
テストケースの階層化 内部クラスを用いてテストケースをグループ分けする (JUnit 5では @Nested アノテーション) グループ分けの観点は後述
テストケースの階層化 多階層にしてもよい 第1階層 第2階層 第3階層 階層が深くなり過ぎたり、 全体のコード行数が大き くなり過ぎたりすると見 通しが悪くなる →複数のテストクラスへ ファイル分割を検討しよ う
階層化のメリット テストランナーで視覚的に構造を俯瞰でき、必要に応じて ドリルダウンできる(鳥の目 /虫の目 )
テストコードのSOS “整理されている(Organized)”
テストケースのグループ分け テストケースを体系的に分類することで、観点の抜け漏れや 網羅性のチェックがしやすくなる (例) 正常系/準正常系/異常系 でグループ分け (例) テストケース数が多い場合に パターンによりグループ分け
(補足)テスト設計の根拠 グループ分けにより体系は把握しやすくなるが、どのような 観点でテストケースを分類したのか、「テスト設計の根拠」 までは表現できない JavaDocコメントとして、テスト 設計の根拠を記すのがオススメ
パラメーター化テスト 同じ内容の検証を行うテストメソッドが多数あると冗長で 見通しが悪いので、パラメーター化テストを導入する テストメソッド本体 パラメーターのソース 1ケース目のパラメータ 複数のテストケースに展開して実行: 2ケース目のパラメータ
テストコードのSOS “自己文書化されている (Self-documenting)”
「自己文書化されている」 テストコードを一瞥しただけで テストの目的や条件が明快にわかること
テストの名称 メソッド名(または表示名)に、日本語(または標準言語 で)でテスト条件と期待する振る舞いを明示する NG例: “割引料金未適用のテスト”
AAA または Given-When-Then Arrange(準備)、Act(実行)、Assert(検証)の 3フェーズに分けて記述する ポイント: • Actは原則1文 • Arrange-Act-Assert-ActAssertのように繰り返す のは絶対NG
可読性を考慮する 実用的なコードだと、ArrangeとAssertは記述が長く冗長に なりがち。とくにテストフィクスチャのセットアップは ヘルパーメソッドに切り出すなど、読みやすさを考慮する。
用語説明:SUT、DoC、テストフィクスチャ SUT (System under test): テストする対象 Doc (Depended-on component): SUTが依存するもの DoC Foo Param テストコード SUT Bar Fixture テストフィクスチャ (Test fixture): テスト実行に必要なものすべて DBのデータや、環境変数、なども含む Baz
大きな振る舞いに対するユニットテスト
小さく分割した振る舞いをテストした後は? アプリケーションサービスに対するテストをどうする? 通常料金を取得する 料金計算 適用可能な割引の中から 最安の料金を判定する アプリケーション 通常料金、割引料金、クープン適用 料金のうち最安の料金を判定する サービス 個々のビジネスロジック
処理フローロジックのテストは必要か? アプリケーションサービスが担う、処理フローロジックのみ を検証することは手間がかかる(モック利用)上に実りが少 ない Aを呼ぶ。 次にBを呼ぶ。 もし〜だったら …という一連の手続きは 内部設計(実装の詳細)であり、 外部の呼び出し側コードの 関心事ではない 料金計算サービスの実装
集合体として提供する振る舞いをテストする より大きな振る舞いを、網羅的にテストするか、否か? →テスト戦略次第だが、基本的には代表的なパターンと エッジケースをテストすれば十分だと考える 料金計算 コンポーネント 料金計算 サービス サービスは 処理フローに専念して いることが前提
テストダブルの使い方
テストダブルとは 実物のコンポーネント(DoC)の代替となるもの 出典: “xUnit Test Patterns” 第11章
間接入力 SUTはDoCとの相互作用により、振る舞いを実現する DoCが返す結果=SUTにとっての間接入力 DoC 直接入力 テストコード SUT 間接入力 DoC
スタブによる間接入力の制御 何らかの理由で間接入力を制御したい場合、スタブを用いる Stub 直接入力 テストコード SUT 間接入力 DoC
いつスタブを使うか スタブを使うと良いケース: ✓サードパーティのライブラリなど、制御困難なDoC ✓複雑なオブジェクトグラフで、セットアップが困難なDoC ✓通常だと発生しない例外をDoCから発生させたい ✓期待する間接入力をDoCから得るための条件が煩雑 Mockitoのメソッドはスタブと モックの区別がないが、変数名で スタブであることを明言
間接出力 SUTが呼び出し元に返却する直接出力の他に、 副作用として発生するもの=間接出力 DoC テストコード SUT 直接出力 間接出力 DoC
モックによる間接出力の観測 何らかの理由で間接出力を観測し、それを検証したい場合、 モックを用いる DoC テストコード SUT 直接出力 間接出力 Mock
いつモックを使うか 使わずに済ませられないか?を考える: ✓それは本当に観測すべきものか? ✓副作用がなくなるように設計を見直せないか? 外部との契約として 観察可能な振る舞いに限り、 モックで検証してOK 出典: “単体テストの考え方/使い方” 第5章
テストダブルの利用指針 ✓まずは、テストダブルを使わずに済むか考える ✓スタブは目的を理解した上で適切に使えばOK ✓モックの利用は極めて慎重に
テストパターン
テストパターン 今日は紹介する時間がないが、テスト 特有のパターンについて代表的なものは 押さえておくとよい 出典: http://xunitpatterns.com/index.html
小まとめ •テストコードのSOSを用いて整理整頓する •小さな振る舞いを網羅的にテストした後、 より大きな振る舞いをテストする •テストダブルに頼り過ぎることなく、うま く活用する
まとめ
Key Message 1. ユニットテストの意義 • ユニットテストは1単位の振る舞いを検証する • 振る舞いの識別と分割(= 設計) • テストコードを書くことは、設計と表裏一体 • テストコードを通して「設計の筋の良さ」を検 証できる
Key Message 2. テスト容易性 • テスト容易性という品質特性は重要 • 「テストコードを書きやすい」という観点 • +「テスト設計をしやすい」という観点
Key Message 3. テストコードを重要な資産として扱う • テストコードのSOS: • 構造化されている(Structured) • 整理されている(Organized) • 自己文書化されている(Self-documenting) • 自動テストの持続可能性のために、テストコー ドにも力を入れよう
参考文献リスト タイトル 著者・訳者 出版社(出版年) アーキテクトの教科書 価値を生むソフトウェアのアーキテクチャ構築 米久保 剛 著 翔泳社(2024) テスト駆動開発 Kent Beck 著 和田 卓人 訳 オーム社(2017) 単体テストの考え方/使い方 Vladimir Khorikov 著 須田 智之 訳 マイナビ出版(2022) xUnit Test Patterns Refactoring Test Code Gerard Meszaros 著 Addison-Wesley(2007)