今(6/7)、JJUG CCC に行っていて、
さっきまで「ユニットテスト基礎講座」を聞いてきたので、
その内容をメモメモ
P.S 16:25ごろ追加
その後のセッション
・単体テストの始め方/作り方
・Mutation Testingのプロジェクト実践
も追加してシェア!
■ユニットテスト基礎講座
アーキテクトの教科書という本を書いている人
Xのアカウントから資料見れる
テスティングフレームワークに触れた人対象
考え方中心
アーキテクトの教科書という本を書いている人
Xのアカウントから資料見れる
テスティングフレームワークに触れた人対象
考え方中心
内容
・ユニットテストの基本概念
・テスト対象
・質の良いテスト
・ユニットテストの基本概念
・テスト対象
・質の良いテスト
●ユニットテスト、書いてますか
8割くらい
ユニットテストを十分にかけていますか
ユニットテストを上手にかけていますか
ユニットテストを書くのは当たり前
書籍など情報源は少ない
ベストプラクティスが浸透していない
●テストコードはAIにかかせればよくね?
事実AIにテストコードは書かせる
→任せっぱなしはよくない
理由①:AIが生成するテストは平均点(かそれに満たない)
理由②:AIは間違える(うそもつく)
テストコードはvibe cordingするべからず
雰囲気でやるのはX
AutomatedTestの知識
●Part1:ユニットテストの基本概念
ユニットテストとは
テストのピラミッド
E2Eテスト
インテグレーションテスト
ユニットテスト
ユニットテストと統合テストの境界はあいまい(解釈による)
Googleでは、共通理解促進のため、テストサイズ(S/M/L)による分類
ユニットテスト
単体と呼ばれる少量のコードを検証すること
実行時間が短い
隔離された状態で実行
古典学派とロンドン学派異なる(テスト駆動開発の本に書いてある)
→古典学派にのっとる
1単位のふるまいを検証
他のテストケースから隔離
ユニットテストの目的
期待通りに正しく動作することを検証する
退行を防ぐ
テストコードという実例を通して理解しやすくする
たんにテストを作成すれば十分ではない→テストの質
テストコードは散らかりやすい(プロダクションコードの数倍)
インテグレーションテスト
ユニットテスト
ユニットテストと統合テストの境界はあいまい(解釈による)
Googleでは、共通理解促進のため、テストサイズ(S/M/L)による分類
ユニットテスト
単体と呼ばれる少量のコードを検証すること
実行時間が短い
隔離された状態で実行
古典学派とロンドン学派異なる(テスト駆動開発の本に書いてある)
→古典学派にのっとる
1単位のふるまいを検証
他のテストケースから隔離
ユニットテストの目的
期待通りに正しく動作することを検証する
退行を防ぐ
テストコードという実例を通して理解しやすくする
たんにテストを作成すれば十分ではない→テストの質
テストコードは散らかりやすい(プロダクションコードの数倍)
テストコードのSOS
構造化
整理
自己文書化
→頭文字でSOS
構造化
整理
自己文書化
→頭文字でSOS
●Part2 テスト対象のふるまい
・1単位のふるまいを識別する
ユニットテストを書かせる:ちゃんと書いている。カバレッジ97
・テストの網羅性は十分か?
振る舞いが大きすぎる
→トランザクションスクリプトはユニットテストに不向き
→分割して統治せよ
小さな振る舞いに分割する
処理フローロジックと中核ロジックに分かれる
関心の分離
因子数が少なくなる
因子水準
パラメータ:因子
パラメータがとる値:水準
→分割して統治せよ
小さな振る舞いに分割する
処理フローロジックと中核ロジックに分かれる
関心の分離
因子数が少なくなる
因子水準
パラメータ:因子
パラメータがとる値:水準
組み合わせテスト
全網羅→爆発
間引き方
2因子網羅→直行表・ペアワイズ
まとめ
1単位のふるまいを識別
分割して統治
テストしやすくなる
全網羅→爆発
間引き方
2因子網羅→直行表・ペアワイズ
まとめ
1単位のふるまいを識別
分割して統治
テストしやすくなる
●Part3 よいテストコードを書くには
S構造化されている
→パッケージ構造
垂直構造(業務観点)でパッケージを設計すべき
Cf水平分割(技術観点)
テストケースの階層化
内部クラスを用いてテストケースをグループ分けする
階層化のメリット
テストランナーで視覚的に構造を俯瞰できる、ドリルダウンできる
O:整備されている
グループ分け
正常系
準正常系
異常系
テスト設計の根拠はわからない:別途のこしておく
→JavaDocコメント
グループ分け
正常系
準正常系
異常系
テスト設計の根拠はわからない:別途のこしておく
→JavaDocコメント
パラメータ化テスト
パラメータを外だし(アノテーションつける)
S:自己文書化されている
一目見ただけで、わかる
テストの名称:テスト条件と期待する振る舞いがわかる
AAAまたはGiven-When-Then
AAA:Arrange準備、Act実行、Assert検証にわけて記述
可読性を考慮する
実用的なコードだと、ArrangeとAssertは記述長くなりがち
SUT:テスト対象の構成要素
Doc:SUTがいジョンするもの
テストフィクスチャー:テスト実行に必要なものすべて
大きな振る舞いに対するユニットテスト
小さく分割した振る舞いをテストした後は
→処理フローロジックのテストは必要か?
単独でテストするのではなく、集合体でテスト(コンポーネント)
集合体として提供する振る舞いをテストする
代表パターンとエッジケースでいいときも
テストダブルの使い方
テスト:ダブル実際のコンポーネントの代替
スタブとモック
パラメータを外だし(アノテーションつける)
S:自己文書化されている
一目見ただけで、わかる
テストの名称:テスト条件と期待する振る舞いがわかる
AAAまたはGiven-When-Then
AAA:Arrange準備、Act実行、Assert検証にわけて記述
可読性を考慮する
実用的なコードだと、ArrangeとAssertは記述長くなりがち
SUT:テスト対象の構成要素
Doc:SUTがいジョンするもの
テストフィクスチャー:テスト実行に必要なものすべて
大きな振る舞いに対するユニットテスト
小さく分割した振る舞いをテストした後は
→処理フローロジックのテストは必要か?
単独でテストするのではなく、集合体でテスト(コンポーネント)
集合体として提供する振る舞いをテストする
代表パターンとエッジケースでいいときも
テストダブルの使い方
テスト:ダブル実際のコンポーネントの代替
スタブとモック
間接入力
SUTはDoCとの相互作用により、振る舞いを実現
DoCが返す結果=SUTにとっての間接入力
スタブを使う
制御困難
複雑なオブジェクトグラフ
通常だと発生しない例外
期待する間接入力
SUTはDoCとの相互作用により、振る舞いを実現
DoCが返す結果=SUTにとっての間接入力
スタブを使う
制御困難
複雑なオブジェクトグラフ
通常だと発生しない例外
期待する間接入力
間接出力
副作用として発生するもの
間接出力の観測:モックを使う
→つかわずにすまないか考える(副作用がなくなるように)
副作用として発生するもの
間接出力の観測:モックを使う
→つかわずにすまないか考える(副作用がなくなるように)
テストダブルまとめ
使わないで済むか
スタブ:適切に
モック:つかわないですむか
テストパターン
テスト特有のパターン
使わないで済むか
スタブ:適切に
モック:つかわないですむか
テストパターン
テスト特有のパターン
小まとめ
SOSの整理
小さな振る舞いを網羅してから大きな振る舞いへ
SOSの整理
小さな振る舞いを網羅してから大きな振る舞いへ
●キーメッセージ
1.ユニットテストの意義
1単位のふるまい
振る舞いの識別と分割:設計と表裏一体
2.テスト容易性
小さく分けるテスト設計しやすい
3.テストコードを重要な資産として扱う
テストコードのSOS
●しつもん
・パッケージソフトのユニットテスト、どこから手を付けたら
レガシーシステム全般に言える
全部テストしたらとんでもない工数
重要な所
変更頻度が高いところ
→テストコードが書けないか、
重要なテストケース
■単体テストの始め方/作り方
(リスペクトです。パクリではないです)
・今回の例:予約注文処理を加えるとき
通常の商品は在庫確認(既存仕様)
予約注文の商品は在庫を確認しないで注文
・まずはテストをかこう
パターンに沿って書く
テスト駆動開発
コードを書く前に失敗する自動テストコードを必ず書く
重複を除去する
・まずは日本語でテストケースを書いてみよう
予約商品を注文すると予約される
AAAパターン
3つのプログラムに分ける
・Arrenge(準備):注文商品が予約商品
③ActとAssertを満たすように
・Act(実行):注文をする
①もっとも簡単なActから書く
・Assert(確認):予約注文として登録されるか
②検証したいことを確認
テストコードと日本語のコメントが1;1で対応する
(リスペクトです。パクリではないです)
・今回の例:予約注文処理を加えるとき
通常の商品は在庫確認(既存仕様)
予約注文の商品は在庫を確認しないで注文
・まずはテストをかこう
パターンに沿って書く
テスト駆動開発
コードを書く前に失敗する自動テストコードを必ず書く
重複を除去する
・まずは日本語でテストケースを書いてみよう
予約商品を注文すると予約される
AAAパターン
3つのプログラムに分ける
・Arrenge(準備):注文商品が予約商品
③ActとAssertを満たすように
・Act(実行):注文をする
①もっとも簡単なActから書く
・Assert(確認):予約注文として登録されるか
②検証したいことを確認
テストコードと日本語のコメントが1;1で対応する
・テストのふるまいは入力と出力が決める
入力:引数
隠れた入力:時刻、状態、外部プロセス(DBなど)
出力:戻り値
隠れた出力:状態、外部プロセス
・隠れた入力と隠れた出力をテストするには
1.公開する
状態を返す
戻り値として
2.テストダブルを使う
DI
・リファクタリングしてできた
・テストはDryよりDAMP:テストはより説明的に
Googleのソフトウェアエンジニアリング
・わかりやすい検証を選ぶ
・まとめ
まずはテストコードから
AAAパターンを日本語で考えよう
入力と出力・隠れた入力と隠れた出力を分けて考えよう
わかりやすいテストコードを加工
DRYよりDAMP
わかりやすい検証を選ぶ
入力:引数
隠れた入力:時刻、状態、外部プロセス(DBなど)
出力:戻り値
隠れた出力:状態、外部プロセス
・隠れた入力と隠れた出力をテストするには
1.公開する
状態を返す
戻り値として
2.テストダブルを使う
DI
・リファクタリングしてできた
・テストはDryよりDAMP:テストはより説明的に
Googleのソフトウェアエンジニアリング
・わかりやすい検証を選ぶ
・まとめ
まずはテストコードから
AAAパターンを日本語で考えよう
入力と出力・隠れた入力と隠れた出力を分けて考えよう
わかりやすいテストコードを加工
DRYよりDAMP
わかりやすい検証を選ぶ
■Mutation Testingのプロジェクト実践
・自己紹介
・問題意識の共有
テストコードを書けば品質が良い?
すべてのふるまいを検証できている?見切れない
カバレッジを通せば品質が良いか?
コードカバレッジでは測れないところ
・MutationTestingとは
(ミューテーションテスト)
ソースコードに意図的なバグ(=ミュータント)を入れる
そのバグを検出できるか
バグ検出できる(Killed(成功)
PIT(PITest)とは
Java向けミューテーションテストツール
バイトコードを操作して、小さな変異(ミュータント)を自動で埋め込む
コードを書き換えても、テストが成功する:テストが弱い
・プロジェクト実践
Spring Boot+MyBatisのレイヤードアーキテクチャ
PITが埋め込む
テストは@SpringBootTestで
PITとの併用可能→遅くなる
→対応:ナイトりービルド、PITのインクリメンタルモード
インフラ層は使えない
DBUにtなどほかのツール併用
CI/CDパイプラインとの統合
GitHub ActionやJenkinsと統合可能だが、プッシュ、プルリク実行するのは非現実的
→スケジュール実行(ナイトビルドなど)
生成AIが作ったテストコード
生成されたテストコードの多くがHappyPath(正常系で記載)
ミュータントが残る→人間が補強、ミューテーションレポートを読みながら…
数日後には変わっているかも
・自己紹介
・問題意識の共有
テストコードを書けば品質が良い?
すべてのふるまいを検証できている?見切れない
カバレッジを通せば品質が良いか?
コードカバレッジでは測れないところ
・MutationTestingとは
(ミューテーションテスト)
ソースコードに意図的なバグ(=ミュータント)を入れる
そのバグを検出できるか
バグ検出できる(Killed(成功)
PIT(PITest)とは
Java向けミューテーションテストツール
バイトコードを操作して、小さな変異(ミュータント)を自動で埋め込む
コードを書き換えても、テストが成功する:テストが弱い
・プロジェクト実践
Spring Boot+MyBatisのレイヤードアーキテクチャ
PITが埋め込む
テストは@SpringBootTestで
PITとの併用可能→遅くなる
→対応:ナイトりービルド、PITのインクリメンタルモード
インフラ層は使えない
DBUにtなどほかのツール併用
CI/CDパイプラインとの統合
GitHub ActionやJenkinsと統合可能だが、プッシュ、プルリク実行するのは非現実的
→スケジュール実行(ナイトビルドなど)
生成AIが作ったテストコード
生成されたテストコードの多くがHappyPath(正常系で記載)
ミュータントが残る→人間が補強、ミューテーションレポートを読みながら…
数日後には変わっているかも
・質問
認証回りのテストは?
セットアップが多いことはある→分離する
ログインとその先でわける
認証回りのテストは?
セットアップが多いことはある→分離する
ログインとその先でわける