単体テストの考え方・使い方を読んだのでまとめます。ちなみにgptと共作です。
基本の理解
単体テストには、古典派(デトロイト派)のアプローチとローマ派のアプローチの2つの方法があります。
古典派のアプローチでは、一連の振る舞いをテスト対象とし、共有依存(dbなどのテスト対象間で依存があるもの)をテストダブルに置き換え、本番のコードが多く使われます。
一方、ローマ派のアプローチでは、コードをテスト対象とし、テスト対象クラスの外部依存関係はテストダブルに置き換え、それ以外の依存を断ち切ります。
両方のアプローチにはそれぞれ長所がありますが、テストでは主にドメインロジックの退行を検出したいため古典派のアプローチが推奨されます。
*モックしてしまうと本来の振る舞いを見逃す可能性があるため。
効果的な単体テストの作成
単体テストを作成する際には、それが開発者だけでなく非開発者にも理解できるようにすることが重要です。なぜなら、開発者でさえ認知負荷に苦しむことがあるからです。
コードではなく、振る舞いをテストしていることを覚えておいてください。したがって、テストメソッドにテスト対象のメソッド名を含める必要はありません。
単体テストの領域では、テスト対象のシステムはしばしば 'sut' (system under test) と呼ばれます。
具体的にはテスト対象のオブジェクトはsutという変数に代入されます。
単体テストの優先事項
良いテストはコードの退行を検出し、リファクタリングに耐え、迅速に実行できるべきです。テストコードを含むすべてのコードは負債です。したがって、テストはリファクタリングへの耐性を最大化すべきであり、つまり、正しいにもかかわらず間違っているというテスト(偽陽性)を減らすべきです。
リファクタリングの耐性がない(テストが間違った結果を出す)と開発者はテストを信用しなくなりテストの意味がなくなる。
テストにおいて信頼性は重要です。テストが偽陽性になりやすくなると、その信頼性が失われます。
モックとスタブ
モックとスタブは、単体テストで使用される2種類のテストダブルです。
モックは外部システムとの通信に使用されます。
モックの中でもスパイはテスト対象のシステムの振る舞いを模倣し、検証します。
一方、スタブは内部で使用され、振る舞いを模倣するだけです。戻り値を定義する任意のオブジェクトはスタブです。ただし、スタブをテストしてはならないことを忘れないでください。なぜなら、それらは実装の詳細だからです。
モックは、呼び出し側が特定の振る舞いを期待している管理外のシステム外依存(IDaaSなど)のみ使用すべきです。管理下のシステム外依存(dbなど)はスタブにすべきです。モックにすると詳細を知りすぎてしまい、テストが容易に壊れてしまいます。
ドメインロジックでは副作用を持つ依存は排除されているのでモックは存在しません。
必然的にモックがあるテストは統合テストもしくはe2eテストになります。
単体テストの手法
単体テストには主に3つの手法があります:
- 値ベースのテスト:副作用がない場合に使用します。戻り値をテストします。
- 状態ベースのテスト:副作用がある場合に使用します。値オブジェクトやヘルパーを使用することで、これらの影響をある程度軽減できます。
- コミュニケーションベースのテスト:副作用がある場合にも使用します。この手法ではモックを作成する必要があり、かなり複雑になることがあります。
可能な限り、値ベースのテストを選択するようにしてください。これは関数型プログラミングスタイルを採用することで達成できます。
*副作用: メソッドシグネチャに示されない隠された出力
例)状態の変更、例外など
リファクタリングと統合テスト
処理速度とドメインの厳守はトレードオフの関係にあります。外部システムへの依存性は一般的にプロセスの始めと終わりに集めるべきです。しかし、ドメインプロセスが停止する可能性がある場合、初期の外部システムプロセス(データベースからの取得など)は冗長になる可能性があります。このような場合、'確認後実行'パターンが有用であるかもしれません。
|ドメインプロセスが停止するとは
class Usecase {
exec() {
user = userRepo.findBy(userId)
// 色々取得
hogeRepo.findBy(xxx)
fugaRepo.findBy(xxx)
// updateで処理が中断する可能性がある
user.update()
userRepo.update(user)
}
}
// domain
class User {
update() {
// メール認証してないユーザーは処理中断
if (!user.emailVerify) return
// ...
}
}
// ------------------------------------------
// 確認後実行パターン
class Usecase {
exec() {
user = userRepo.findBy(userId)
if (!user.canUpdate()) return
hogeRepo.getBy(xxx)
// ...
}
}
// domain
class User {
update() {
if (!user.canUpdate()) return
// ...
}
canUpdate() {
if (!user.emailVerify) return false
// チェック
}
}
統合テストは通常、外部プロセスとの統合を伴います。これらをモックに置き換えるかどうかは、依存性が制御下にあるかどうかによります。制御下にある場合はそのまま使用し、制御外であればモックに置き換えるべきです。
単体テストのアンチパターン
- プライベートメソッドのテストは避けてください。これらは実装の詳細であり、テストを壊れやすくし、リファクタリングに対する耐性を下げる可能性があります。
- テストでは常に具体的な値を使用し、テストにドメインが漏洩することを防ぐようにしてください。
Tips
サードパーティ製のツールをwrapするアダプタを作成する。
利点として必要なクラスや関数のみ公開できる、モック作るのが楽などがあります。