はじめに
こんにちは。保守性の高いソフトウェアの開発を目指すエンジニア @blue32a です。 辛い開発をなんとか解決して未来につなげるため、主にソフトウェア設計からアプローチしています。
近年はテストコードを書く機会が増えてきたのですが、その時にモックを使うことがあります。モックは便利ですが、適切に使えないと自動テストを効果的に行うことができません。
そこで、この記事では「モックの適切な使い方」について検討しつつ、自分の理解を整理していきたいと思います。
「モック」とは何か
まず、ソフトウェアテストにおける「モック」について確認していきましょう。
ソフトウェアテストの文脈において登場する「モック」は、多くの場合「テストダブル(Test Double)」というものを指していることが多いです。そしてテストダブルは「テスト対象が依存するコンポーネントの代用品」のことです。
この場合のモック(テストダブル)の利用例としては、データベースやWeb API(またはそれを操作するためのクライアント)の代用品として使われることが多いと思います。
しかし、テストダブル以外の別のものを指して「モック」と呼んでいる場合もあります。例えば次に挙げるものです。
- テストダブルの分類の一つであるMock Object
- モック・ライブラリから提供されるモック・オブジェクトを作成するためのクラス
このようにソフトウェアテストにおいて「モック」と呼ばれるものがいくつかあります。モックについて議論する際には、「対象となるモックが何を指しているのか」というのを明確にしておくのが良さそうです。
この記事で扱うモックは分類の一つや特定のツールや技術ではなく、広い意味でのモック、つまりテストダブルについて扱います。(以降はテストダブルという用語を使用します)
- ソフトウェアテストで「モック」と呼ばれるものがいくつかある
- 多くの場合はモック=テストダブルを指している
- 「モック」が何を指しているのか、明確にすることが大切
テストダブルについて
もしかしたら「テストダブル」という用語が耳慣れないかもしれません。本題に入る前に、テストダブルについても少し整理しておきます。
この「テストダブル(Test Double)」という用語は『xUnit Test Patterns』において、それまで曖昧だったモックに関する用語を定義したものです。名前の由来は映画のスタントマン(Stunt Double)から来ています。
このテストダブルは代用品の総称で、細かくは次の5つの分類があります。
- スタブ(Stub)
- スパイ(Spy)
- モック(Mock)
- フェイク(Fake)
- ダミー(Dummy)
普段全てを意識することはないのですが、例えばテストフレームワークを使っていて目にするものもあると思います。(例: jest.spyOn(...)
)
この5つの分類は、大きく2つに分けて見ることができます。
- モック(モック、スパイ):テスト対象から依存へと向かう、外部に向かうコミュニケーション(出力)を模倣し、検証するのに使われる
- スタブ(スタブ、ダミー、フェイク):依存からSUTへと向かう、内部に向かうコミュニケーション(入力)を模倣するのに使われる
このことから、テスト対象とテストダブルの間のコミュニケーションの方向や入出力に注目するのが良さそうです。
テストダブルのメリット・デメリット
続いて、モック改め「テストダブルの適切な使い方」について考えるにあたって、テストダブルのメリット・デメリットを整理していきます。
まずはメリットからです。
テストダブルを使うメリット
1. テストしにくいものをテスト可能にする
「テストしにくいもの」は例えば次のようなものがあります。
- 直接制御できない依存(例:外部のプロセス、テスト容易性の低いコンポーネント)
- 意図的に発生させることができない動作(例:異常系)
- テストで実行することが難しい操作(例:メール送信、決済)
テストダブルを使うことで、これらのテストを行うことが可能になります。
2. テストの実行速度が向上する
テストダブルを使うことでネットワーク通信や大量のデータを扱う処理を省くことができ、その結果として実行速度が向上します。
テストの実行速度を上げることは、フィードバックを早く得るために重要なことです。テストの実行が早ければ、思考を中断せずにテストを行うことができるので、実行の頻度を増やすことができます。
3. テストの決定性が向上する
「決定性が高い」とは、同じ入力に対して同じ出力が返ることです。逆に決定性が低くなると同じ入力に対して同じ結果が返らないので、「何も変更していないのにテストが成功したり失敗したりする」ということが起こり得ます。
決定性が低くなる例は乱数生成器やシステム日時を使った処理です。これらをテストダブルに置き換えることにより、入力に対する期待値との比較がやりやすくなります。
まとめると、テストダブルを使うことによってテストの実行が容易になる、というメリットがあります。
続いてデメリットです。
テストダブルを使うデメリット
テストコードが長くなる
テストダブルを作成するコードは、普通に依存を作成するよりも長いコードになりがちです。
検証(Assert)のコードが分散する
テストコードを読みやすくするため、Arrange(準備)、Act(実行)、Assert(確認)の3つのフェーズに分けて記述するAAAパターンなどのプラクティスがあります。
しかし、テストダブル作成の中に検証ロジックが含まれたりするので、テストコードをAAAパターンのように整理することが難しくなります。
テストが壊れやすくなる
テスト対象が内部的に使う依存を制御するので、実装の詳細に注目することになります。これにより、振る舞いを変えずに実装を変える、いわゆるリファクタリングへの耐性が低下することになります。
本物の動きと同一である保証はない
テストダブルの動作は開発者の想定で決めていくので、本物が同じように動く保証はありません。また本物の動作が変更された時、テストダブルはその変更に追従してくれません。
まとめると、テストダブルを使うことによって保守性が低下したり、テスト結果に誤りが出る可能性が増える、というデメリットがあります。
- テストダブルのメリットは「テストの実行が容易になる」
- テストダブルのデメリットは「保守性やテストの信頼性の低下」
テストダブルの適切な使い方について検討する
前のセクションではテストダブルのトレードオフを確認しました。それを下に適切な使い方について考えていきますが、その前に基本方針について確認しておきます。
世の中にはテストダブルを「積極的に使う派」と「あまり使わない派」があります。現在の私は「あまり使わない派」なので、「テストダブルのメリットについて、テストダブル以外の代替手段を考えつつ使用を検討する」という形で検討していきたいと思います。
テストの決定性を確保したい
テストダブルを使うことで「決定性の低い依存」を使ったテストの決定性を向上させることができます。
まずは「決定性の低い依存」を使ったコードの例を見てみましょう。
class HolidayChecker {
public function isHoliday(): bool {
$today = new \DateTime(); // 現在日時の取得
$result = /* 祝日の判定 */;
return $result;
}
}
class SpecialOfferService {
private $holidayChecker;
public function __construct(HolidayChecker $holidayChecker) {
$this->holidayChecker = $holidayChecker;
}
public function getDiscount(): float {
return /* HolidayCheckerを使って割引率を返す */
}
}
このコード例ではHolidayChecker
が現在日時を取得しているため「決定性が低い依存」になっています。
HolidayChecker
をそのまま使ってテストでは、テストを実行した日付によって結果が変わってしまうことになります。
これを解決するため、HolidayChecker
をテストダブルに置き換えてisHoliday()
の結果を制御することでテストの決定性を高めることができますが、テストダブルを使わない別の方法があります。
HolidayChecker
を次のようにリファクタリングしてみましょう。
class HolidayChecker {
public function __construct(private \DateTime $today) {}
public function isHoliday(): bool {
$today = $this->today; // コンストラクタから渡された日時にアクセス
$result = /* 祝日の判定 */;
return $result;
}
}
このように現在日時を引数として渡すことで、テストダブルを使わずに制御することができるようになり、入力に対して常に同じ結果を返ることになります。
このように、依存性の注入(Dependency Injection)を活用することができる場合、テストダブルのデメリットを回避しつつ、テストの決定性を確保することができます。
テストの実行速度を向上させたい
時間がかかる処理をテストする場合、時間がかかる依存をテストダブルに置き換えることで改善したいと思うかもしれません。
ここではテストダブルを使う以外の方法で実行速度に対するアプローチを考えてみます。
テストを並列実行する
テストフレームワークに並列実行する機能があれば、それを活用することで実行速度を改善が期待できそうです。
テストのグループ化と実行タイミングの調整
例えば特定の機能を開発している時にテストを実行することを考えると、「関係するテストだけで十分」ということもあります。そのような場合はテストをグループ化しておくことで、特定のテストのみを実行することができます。
そうしておいて、時間がかかるテストは特定のタイミングやスケジュールで実行するような方法が検討できます。
設計の改善
例えば、次のようなデータベースへのアクセスを行う処理について考えます。
class ReportService {
public function __construct(private \PDO $pdo) {}
public function getReport(): Report {
// データベースからデータを取得
$stmt = $this->pdo->query(/* SQL */);
$reportDetails = $stmt->fetchAll(\PDO::FETCH_ASSOC);
// 明細データを変換してレポートを作成
return $this->createReport($reportDetails);
}
private function createReport(array $reportDetails): Report {
// ...
}
}
このような実装の場合、レポート作成に関するテストではデータベースへのアクセスを行うことになります。ここでデータベースへアクセスするオブジェクト(PDO
)をテストダブルに置き換えることもできますが、設計を改善することで解決してみます。
class ReportService {
public function __construct(private \PDO $pdo) {}
public function getReport(): Report {
// データベースからデータを取得
$stmt = $this->pdo->query(/* SQL */);
$reportDetails = $stmt->fetchAll(\PDO::FETCH_ASSOC);
// 明細データを変換してレポートを作成
return Report::create($reportDetails);
}
}
class Report {
public static create(array $reportDetails): Report {
// ...
}
}
このように、単一責任の原則に則ってリファクタリングすることにより、レポート作成のテストはデータベースへのアクセスが必要なくなります。
コンテナ技術の活用
Dockerなどのコンテナ技術を使うことで、データベースやWebサーバーを同じマシンの中で動かすことができます。完全な本物とは異なりますが、外部とのネットワーク通信を避けることで速度に対する問題を改善しつつ、本物に近いものを使うことでのデメリットも緩和することを目指します。
テストしにくいものをテストしたい
異常系
例えばネットワークエラーなどの異常系は安定して意図的に発生させるこは困難だと思います。ですので、このようなテストはテストダブルを使うことが有効だと思います。
外部のプロセス
書籍『単体テストの考え方/使い方』によると、外部のプロセスには2種類に分類できるようです。
- 管理下にある依存(managed dependency):テスト対象のアプリケーションが好きなようにすることができるプロセス外依存(例:テスト対象のアプリケーションしかアクセスしないデータベース)
- 管理下にない依存(undmanaged dependency):テスト対象のアプリケーションが好きなようにすることができないプロセス外依存(例:メールサービス)
管理下にある依存については、処理の結果を自身で観測することができるため、本物を使ったテストを考えることができます。
しかし、管理下にない依存は結果を観測することが出来ないため、テストダブルを使うことが有効だと思います。
テスト容易性の低いモジュール
レガシーなシステムにテストを書こうとする場合や、テストを書くことに慣れていない場合、このようなモジュールをテストダブルに置き換えることがあります。しかしこれには注意が必要です。
「テスト容易性が低い」というのはプロダクトコードの設計について改善を検討する必要があります。しかし、モックを使うとそのような設計の問題を覆い隠してテストをすることが出来てしまいます。
テストダブルのデメリットで確認したように、テストダブルを使うと保守性やテストの信頼性が低下していきます。そうなると、変更を容易にするために自動テストを導入したはずが、変更を妨げる要素になってしまいます。
まとめ
今回はソフトウェアテストにおけるモックの適切な使い方について検討してみました。
- ソフトウェアテストにおける「モック」とは、主に「テスト対象が依存するコンポーネントの代用品=テストダブル」を指す
- テストダブルのメリットは「テストの実行が容易になる」
- テストダブルのデメリットは「保守性やテストの信頼性の低下」
- テストダブルに期待するメリットの多くは、代替手段を検討することができる
- 処理の結果を観測することができない「管理下にある依存(managed dependency)」についてはテストダブルに置き換えることが有効
- テストダブルへの置き換えを考える時は、設計に改善すべき問題が無いかを考える
テストダブルは強力ですが、それゆえ適切な使い方が求められます。テストダブルを有効に活用してテストを充実させ、変更が楽で安全な開発を目指していきましょう。
この記事の内容が参考になれば幸いです。