「単体テストの考え方/使い方」勉強会02 〜モックの概要・使いどころ〜
Specteeでエンジニアをしている和山、峯田、國久、永野です。
私たちが所属している部署では、有志でいくつかの勉強会を立ち上げています。
今回は書籍「単体テストの考え方/使い方」の輪読会から テスト・ダブル(特にモック) の使い方を紹介します。
本記事は数回に分けて輪読会参加メンバーが執筆しています。
前回の記事(理想的なテストと壊れやすいテスト)はこちら。
なお、本記事のサンプルコードはC#
となります。
テスト・ダブルとは
テスト・ダブルは、プロダクション・コードには含まれず、テストでしか使われない偽りの依存を表現するもので、テストを円滑に実施するために用いられます。
映画のスタントマン(Stunt Double)を語源としているようです。
モックとスタブの違い
テスト・ダブルは大きくモックとスタブの2つに分類することができます。
これまでは双方の違いを理解できておらずモックと一括りにしていましたが、本書を読んでその違いを理解できました。
モック
モックは外部に向かうコミュニケーションで使用されます。
外部に向かうコミュニケーションとは、テスト対象システムが依存に対してその依存の状態を変えるために行う呼び出しのことです。
例としてメール送信が挙げられます。
モックはこのようなコミュニケーションを模倣し、検証を行います。
モックのコード例
下記のテストは、テスト対象システムからモックに対して行われた呼び出しを検証しています。
[Fact]
public void Sending_a_greetings_email() {
// Arrange: モックの作成と、テスト対象システムのインスタンス化
var mock = new Mock<IEmailGateway>();
var sut = new Controller(mock.Object);
// Act: GreetUserメソッドの呼び出し
sut.GreetUser("user@email.com");
// Assert: SendGreetingsEmailメソッドが1回呼び出されることを検証
mock.Verify(x => x.SendGreetingsEmail("user@email.com"), Times.Once);
}
スタブ
スタブは内部に向かうコミュニケーションで使用されます。
内部に向かうコミュニケーションとは、テスト対象システムが依存からデータを取得するために行う呼び出しのことです。
例としてデータベースからの読み取りが挙げられます。
スタブはこのようなコミュニケーションを模倣します。
スタブのコード例
下記のテストは、事前にスタブが返す結果を設定しておき、テスト対象システムの呼び出しの出力結果を検証しています。
スタブに対して、GetNumberOfUsers()
が呼び出された場合、常に10
を返すように設定しています。
[Fact]
public void Creating_a_report() {
// Arrange: スタブが呼び出されたときに返す結果を事前に設定
var stub = new Mock<IDatabase>();
stub.Setup(x => x.GetNumberOfUsers()).Returns(10);
var sut = new Controller(stub.Object);
// Act: レポートの作成
// CreateReportメソッド内部では、IDatabaseのGetNumberOfUsersメソッドが呼び出されることが予想される
Report report = sut.CreateReport();
// Assert: 出力結果が10であることを検証
Assert.Equal(10, report.NumberOfUsers);
}
テスト・ダブルの利用とテストの壊れやすさの関係
テストのリファクタリングへの耐性を高く保つためには、テスト・ケースが実装の詳細を扱わず、観察可能な振る舞いを検証することです。
- 実装の詳細
- テスト対象の内部的なコード
- 具体的な実装方法やアルゴリズム
- 観察可能な振る舞い
- テスト対象が生み出す最終的な結果
- 外部から観察可能な動作や出力
このテスト・ケースが実装の詳細を扱わず、観察可能な振る舞いを検証するという原則は、テスト・ダブルを用いる場合でも守る必要があります。
テスト対象システムのカプセル化を無視して、内部メソッドの呼び出しをモック化することは、テスト対象システムの実装の詳細に深く結びつくため、壊れやすいテストに繋がります。
観察可能な振る舞いをテストするコード例
下記のテストは、外部アプリケーションのメールサービスのみモック化しています。
テスト対象システムの観察可能な振る舞いであるCustomerController.Purchase
の出力結果と、メール送信の検証をしています。
[Fact]
public void Successful_purchase()
{
// Arrange: メールサービスをモック化と、テスト対象システムのインスタンス化
var mock = new Mock<IEmailGateway>();
var sut = new CustomerController(mock.object);
// Act: Purchaseメソッドの呼び出し
bool isSuccess = sut.Purchase(customerID: 1, productID: 2, quantity: 5);
// Assert: 呼び出し結果の真偽値とメールサービスのメソッドが実行されたことを検証
Assert.True(isSuccess);
mock.Verify(x => x.SendReceipt("customer@example.com", "Shampoo", 5), Times.Once);
}
実装の詳細をスタブ化しているコード例
下記のテストは、HasEnoughInventory()
やRemoveInventory()
がCustomer.Purchase()
の中で直接呼び出されることを前提としています。
このテストはすぐに壊れる可能性があります。
例えばCustomer.Purchase
の実装が少しでも変更され、HasEnoughInventory()
を呼び出さない方法や新しいメソッドを追加する形で処理が変更された場合です。
[Fact]
public void Purchase_succeeds_when_enough_inventory()
{
// Arrange: モックが呼び出されたときに返す結果を事前に設定
var storeMock = new Mock<IStore>();
storeMock
.Setup(x => x.HasEnoughInventory(Product.Shampoo, 5))
.Returns(true);
var customer = new Customer();
// Act: Purchaseメソッドの呼び出し
bool success = customer.Purchase(storeMock.Object, Product.Shampoo, 5);
// Assert: 呼び出し結果の真偽値とRemoveInventoryメソッドが実行されたことを検証
Assert.True(success);
storeMock.Verify(
x => x.RemoveInventory(Product.Shampoo, 5),
Times.Once);
}
感想と実践:モックとスタブの違いについて理解を深められた
テスト・ダブルを用いた場合でも、リファクタリングへの耐性を保ちながら効果的なテストを実装する方法が理解できました。輪読会を通して、チーム内でモックとスタブの違いや、壊れにくいテストを作るための観点についての共通認識を深めることができたのも良かったです。
実際、私たちが管理するコードでは、本来スタブの役割であるものにmock
という誤った命名がされていたり、実装の詳細と結びついたモックが作成されていたことが判明しました。この点を修正することで、テストの保守性を向上させることができました。
さらに、良いテストを作成するためには、アーキテクチャ設計において依存関係を適切に抽象化し、実装の詳細を外部に漏らさないようカプセル化することが重要であると学びました。
モックをいつ使用するべきか
ここまでで、テスト・ダブルの概要やモックの使い方を整理しました。
ここからは具体的をどのような場面でモックを使用すべきかについて解説します。
モックは統合テストにおいて、管理下にない依存にのみ使うべき
本書では 「統合テストにおいて、管理下にない依存に対してのみモックを使うべき」 としています。
前提として単体テストではプロセス外依存を使わないように設計します。
大まかなコード設計はドメイン・モデルとコントローラーに分離させ、コントローラー側にプロセス外依存をもたせます。
そして、ドメイン・モデルを単体テストで、コントローラーを統合テストで検証します。
管理下にある依存とない依存の違いは以下の通りです。
- 管理下にある依存: テスト対象のアプリケーションが好きなようにすることができるプロセス外依存。
例としてテスト対象のアプリケーションしかアクセスしないデータベースがあげられる。 - 管理下にない依存: テスト対象のアプリケーションが好きなようにすることができないプロセス外依存。
例として、メール・サービスやメッセージ・バスがあげられる。
統合テストで管理下にある依存をそのまま使えば、対象アプリケーションが最終的にどのような状態になったのかを外部の視点で検証しやすくなります。
また、管理下にある依存をそのまま使うことで、データベースのリファクタリングも行いやすくなるという主張もありました。
管理下にない依存に対してのみインターフェイスを用意する
モックを使用するために、インターフェイスの準備が必要になります。
ここで、本書は 「インターフェイスは管理下にない依存に対してのみ用意する」 という思想強めの主張があります。
インフラ層においてはインターフェイスと実装クラスの組み合わせをよく使います。
// IMessageBusインターフェイスとMessageBusクラスを定義
public interface IMessageBus
public class MessageBus : IMessageBus
// IUserRepositoryインターフェイスとUserRepositoryクラスを定義
public interface IUserRepository
public class UserRepository : IUserRepository
このようなインターフェイスの使い方について本書は以下のような反論をしています。
- インターフェイスを導入しても、具象クラスをそのまま使う場合と比べて疎結合になるわけではない。(インターフェイスと具象クラスが1:1の関係)
- 本来、抽象化は、発見することであり作り出すことではない。
- 将来的に追加される機能を予想して開発する観点はYAGNI原則から外れることになり、機会に対するコストがかかってしまう。
管理下にない依存に対しては統合テスト時にモックを作る必要があるためインターフェイスを準備します。
ただし、インターフェイスに対して複数の実装クラスができるような本当の意味での抽象化が行われる場合は、モックにするかどうか関係なくインターフェイスを使っても構わないとの主張もありました。
感想と実践:コード設計にもよるから盲信は禁物
モックの使用方法は本書の主張通りにやってみたいなと思いました。
今まで「ここはモック。ここは具象クラス」みたいに感覚的に実装していましたが、管理下にある依存とない依存の区分けで整理して実装していきたいと思いました。
インターフェイスの話は輪読会メンバーの中で賛否両論でした。
今まで学習した中には無かった、新しい観点のインターフェイスに対する考え方だなと思いつつ
アーキテクチャや実装パターンによっては「1:1の関係や管理下にあるプロセス外依存に対してもインターフェイスを使ってよいのでは?」といった意見も上がりました。
例えば本書でも触れられていたOCPを厳格に守ろうとする場合、どうしてもインターフェイスによる安定化を行う場面がでてくると思います。
YAGNI原則についてもどこまでが適用範囲なのか明確な線引きがなく、原則に対してチームで議論が必要です。
ここの節はインターフェイス利用のメリット・デメリットをチームで整理する良いきっかけとなりました。
まとめ:モックの利用は計画的に
本書を通じてモックとスタブの使い分けや、実際にどの場面で利用すべきかについての理解をすることができました。
以下本記事の簡単なまとめです。
- モック: 外部に向かうコミュニケーションを模倣し、検証を行う。
- スタブ: 内部に向かうコミュニケーションを模倣する。
- モックは統合テストにおいて、管理下にない依存に対してのみ使うべき
適切なモックの利用でテストの価値を高めたいと思います!
Discussion