初めに
株式会社ナイトレイの和田です。普段、自社サービスの開発に取り組んでいます。
現在私が関わっているプロダクトにユニットテストを導入しました(そのプロダクトではユニットテストが省略されていました)。依存箇所はモックに置き換えてテストすることになり、その際に学習したテストタブルについて共有します。
テストダブルとは
ソフトウェアテストにおいて、SUTが依存しているコンポーネントを置き換える代用品のこと。
💡 SUTとは、「テストしている対象」を示すもの。ユニットテストの場合、テストスクリプトが実行する「テスト対象のクラスやメソッド」のことを指す。
5種類のテストダブル
①スタブ
スタブとは
SUTの依存コンポーネントを置き換えて、都合の良い任意の値を返すテストダブルのこと。
スタブを利用するメリットは以下が挙げられる。
- テスト対象が意図通りに動くか?をテストできる(=SUTが依存コンポーネントの出力に左右されずに意図した出力ができるかどうかをテストできる)
- 依存コンポーネントの部分に決まりきった正しい挙動をするオブジェクトを注入することで、例えテストが失敗しても「他の何か」ではなく、そのSUTに問題があることが分かる
補足
依存コンポーネントは他の場所でテストされているはずだし、その正しさまでテストすることは責務外のことである。
テスト例
*ここでのテスト例は、「PHPUnitでスタブとモックを理解する!【テストダブル】」という記事にあるものをそのまま引用させていただいています。
SUT
以下のようなSUTがあったとする。
- sometimesErrorメソッドの返り値によって結果が変わる
- ランダムな値を返す
- エラーの場合は0を返し、catchに入る
function sumOfArraySomeTimesZero(array $nums, CheckNumsIF $check_nums_obj): int
{
try {
// ランダムでエラーを返すメソッド
$check_nums_obj->somtimesError();
return array_sum($nums);
} catch (Throwable $e) {
return 0;
}
}
これは単体テストがしずらい。
理由としては、somtimesErrorメソッドの結果によって返り値が異なるため(=依存している)。
テストコード
正常系パターン
以下の手順でsomtimesError()がエラーを返さないようにスタブを準備する
- createStub()を使い、引数にスタブにしたい対象クラスを渡す
$stub = $this->createStub(CheckNums::class);
- スタブに任意の値を返させる
- method()で引数に操作したいメソッドを渡す
$stub->method('somtimesError');
- その他にも、willReturn()で返り値を設定できる
- 今回は何も返さないようにする
- method()で引数に操作したいメソッドを渡す
public function testSumOfArrayWithStub(): void
{
$nums = [1, 2];
// CheckNumsクラスのスタブを作る
$stub = $this->createStub(CheckNums::class);
// 何も返さないスタブの設定を行う
$stub->method('somtimesError');
// テスト対象のメソッドを実施すると、スタブを返す
$result = sumOfArraySomeTimesZero($nums, $stub);
$this->assertSame($result, 3);
}
準正常系パターン
somtimesError()がエラーを返すパターンをテストするためのスタブを用意する。
public function testSomeTimesZeroWithStub(): void
{
$nums = [1, 2];
// CheckNumsクラスのスタブを作る
$stub = $this->createStub(CheckNums::class);
// 例外を出力するスタブの設定を行う
$stub->method('somtimesError')
->will($this->throwException(new Exception));
$result = sumOfArraySomeTimesZero($nums, $stub);
$this->assertSame($result, 0);
}
②モック
モックとは
SUTの依存コンポーネントを置き換えて、そのコンポーネントが正しく呼び出されているかを検証するために用意するテストダブルのこと。
モックを利用するメリットは、SUTの依存コンポーネントが意図通りに動くか?をテストできること(=実際にSUTの依存コンポーネントを実行せずに、呼び出した回数などをテストできる)。
テスト例
*ここでのテスト例は、「PHPUnitでスタブとモックを理解する!【テストダブル】」という記事にあるものをそのまま引用させていただいています。
SUT
あるAPIを実行するメソッド。
ランダムなユーザーデータを生成してくれる無料のオープンソースAPIを利用。
function getUserData(UserApiService $userApiService): void
{
$url = "https://randomuser.me/api/";
// APIの実行を行うメソッド
$userApiService->curl_test($url);
// なんらかの処理など
}
テストコード
テストの度に毎回APIが走るのは避けたいので、モックですり替える。
モックを利用し以下を検証する。
- APIが実際に1回だけ実行されているか?(2回呼ばれてたらまずい)
- 引数に適切なurlが指定されているか?(タイポなどのミス)
public function testGetUserData(): void
{
$url = 'https://randomuser.me/api/';
// UserApiServiceクラスのモックを作る
$mock = $this->createMock(UserApiService::class);
$mock->expects($this->once()) // 1度だけ呼ばれるか
->method('curl_test') // 対象メソッド
->with($url); // 引数が指定の値になっているか
getUserData($mock);
}
③スパイ
スパイとは
スパイはスパイとSUT間のやり取りを記憶し、後ほどやり取りをアサートできるようにするテストダブルのこと。
モックと同じように、テスト対象の依存コンポーネントが意図通りに動くか?をテストするためのテストダブル。
モックと異なる点
- モックは対象の処理の途中にリアルタイムに検証するのに対し、スパイはテスト対象の一連の処理をひととおり実行した後に検証する
- モックは全てのエクスペンションを検証しなければならないが、スパイの場合は調べたいエクスペンションのみ検証できる
メリット
- シンプルになり、テストコードが読みやすい
- 検証したい項目が明確化する
デメリット
- リファクタリングの必要性が浮き彫りにならない
- モックの場合、全てのエクスペンションを指定する必要があるため、そのSTUが冗長であることを発見できる
- スパイの場合、SUTの設計の一部を隠してしまう
- デバッグしにくい
- モックの場合、期待しない呼び出しを受けるとその時点ですぐに例外を投げ、きれいなスタックトレースか、デバッガーを起動することさえある
- スパイの場合、呼び出し後に検証するので、モックのように「その時点で」「同じような」エラー情報を得ることができない
- テストダブルに返り値を定義する必要がある場合、スパイでは行えない
コード例
*PHPモックオブジェクトフレームワークである「Mockery」を使って説明します
*ここでのテスト例は、「Mockery1.0 テストダブル作成」というMockeryのドキュメントにあるコードをそのまま利用させていただいています
モックの場合
// arrange(準備)
$mock = \Mockery::mock('MyDependency');
$sut = new MyClass($mock);
// expect(期待)
$mock->shouldReceive('foo')
->once()
->with('bar');
// act(実行)
$sut->callFoo();
// assert(アサート)
\Mockery::close();
スパイの場合
// arrange(準備)
$spy = \Mockery::spy('MyDependency');
$sut = new MyClass($spy);
// act(実行)
$sut->callFoo();
// assert(アサート)
$spy->shouldHaveReceived()
->foo()
->with('bar');
④フェイクオブジェクト
SUT の依存コンポーネントの代品として動作し、本物のコンポーネントと同等の挙動をするもの。
テストスパイやモックオブジェクトとは異なり、フェイクオブジェクトは**「検証」のために使用するものではない**。
使い所
- 本物のコンポーネントが未実装でまだ利用できない
- 本物のコンポーネントを使うとデータの変更や削除等の望ましくない副作用が発生する
- 本物のコンポーネントを使うとテストが大幅に遅くなる
フェイクオブジェクの代表例
- テスト中のみ使われれるインメモリのストレージ
- 外部の API に対応するゲートウェイクラス
⑤ダミーオブジェクト
テスト中で SUT の利用に必要なコンポーネントの代用品。
ただし、ダミーオブジェクトは、スタブ・スパイ・モック・フェイクオブジェクトで挙げた他のものとは異なり、何の機能も備えていない。
テスト対象の状況を作り出すのに便利ではあるが、 SUT がダミーオブジェクトを利用して動くわけではない。
その意味で、有名なユニットテストの書籍である『 xUnit Test Patterns: Refactoring Test Code 』( Gerard Meszaros 著)では「ダミーオブジェクトはテストダブルではない」と説明されている。
ダミーオブジェクトの例としては、関数の引数の条件を揃えるためだけに渡す実引数などがある。
補足
定番のテストダブルライブラリでは、スパイとモックがスタブの機能を兼ね備えていることもある。 その場合は次のとおりになる。
- スタブ: 指定された挙動をする機能
- スパイ: (スタブの機能) + 記録機能
- モック: (スタブの機能) + 処理中の検証機能
参考
最後に
最後に
私たちの会社、ナイトレイでは一緒に自社開発のWebサービスを盛り上げてくれるエンジニアメンバーを募集しています!
基本的には直接ユーザーと接することのないポジションですが、セールス部門から
「顧客の声」を教えてもらったり、希望すればユーザーとのMTGに参加することも可能です。
モチベーションの高め方はあなた次第。
このような方は是非Wantedlyからお気軽にご連絡ください(もしくはこちらまで recruit@nightley.jp )
✔︎ 自社Webサービスの開発で事業の発展に携わってみたい
✔︎ 自分が開発したサービスで地域活性化に貢献したい
✔︎ 位置情報ビッグデータに興味があり、新しい活用方法を提案したい
✔︎ 地理や地図が好きで仕事中も眺めていたい
一つでも当てはまる方は是非こちらの記事をご覧ください
「受託開発一切なし!自社WEBサービスを社長やセールスチームと一緒に開発!」 https://www.wantedly.com/projects/467606
▼ナイトレイとは?