はじめに
PHPのモックフレームワークであるMockeryを使ったテストコードとサンプルケースで、モックとスタブの違いを自分なりに噛み砕いて説明してみます。
対象読者はMockery使ったことあるけどいまいちモックとスタブの違いがわからない人です。
モックとは
テスト対象が依存している他のクラスのメソッドを、テスト対象が期待通り呼び出しているか検査するためのもの。
似てるけど違う、スタブとは
テスト対象が依存している他のクラスの挙動に影響を受けないように、事前に定められた応答をするもの。
モックと違うところは、検査したい観点が違う。モックはテスト対象が依存クラスを使って期待したメッセージングをしているか確認する。
わからん
コードで説明してみる
Aさんは「2つの整数を足し算して答えが3の倍数ならアホになる」振る舞いをする人だと仮定します。
Aさんは「計算できる何か」が無いと足し算できません。「計算できるなにか」に依存している状態です。
コードで表すと下のようになります。
class Aさん
{
private $計算できるなにか;
/**
* これをテストしたい
*/
public function 足し算して答えが3の倍数ならアホになる(int $number1, int $number2): string
{
$result = $this->計算できるなにか->足し算する($number1, $number2);
return $result % 3 === 0 ? 'アホになった' : 'アホじゃない';
}
public function __construct(計算できるなにか $計算できるなにか)
{
$this->計算できるなにか = $計算できるなにか;
}
}
計算できる何かのコードは下記です。
interface 計算できるなにか
{
public function 足し算する(int $number1, int $number2): int;
}
class 電卓 implements 計算できるなにか
{
public function 足し算する(int $number1, int $number2): int
{
return $number1 + $number2;
}
}
Aさんのやることは2つあります。
- 計算できるなにかに、足し算を依頼する
- 答えが3の倍数か判断して、アホになるかどうか判断する
テストを書いていきましょう
class Test_Aさん extends TestCase
{
public function test_2つの整数を足し算して答えが3の倍数ならアホになること()
{
$電卓 = new 電卓();
$Aさん = new Aさん($電卓);
$アホじゃないと予想 = 'アホじゃない';
$this->assertEquals($アホじゃないと予想, $Aさん->足し算して答えが3の倍数ならアホになる(1, 1));
$アホになってる予想 = 'アホになった';
$this->assertEquals($アホになってる予想, $Aさん->足し算して答えが3の倍数ならアホになる(1, 2));
}
}
良さそうに見えるけど・・・
もし「電卓」にバグがあり正しく動作していなかったら、Aさんのテストも失敗してしまいます。
Aさんは悪くなくて、電卓が悪いのに・・・
じゃあどうすれば・・・
ここで使うものがスタブです。
事前に定められた応答をするものを準備することで、電卓にバグがあるかどうかに関係なくAさんのテストができます。
class Test_Aさん extends TestCase
{
public function test_2つの整数を足し算して答えが3の倍数ならアホになること()
{
$電卓 = Mockery::mock(計算できるなにか::class);
// 足し算するが1回目呼ばれたら2を必ず返す。足し算するが2回目呼ばれたら必ず3を返す
$電卓->shouldReceive('足し算する')->andReturn(2, 3);
$Aさん = new Aさん($電卓);
$アホじゃないと予想 = 'アホじゃない';
$this->assertEquals($アホじゃないと予想, $Aさん->足し算して答えが3の倍数ならアホになる(1, 1));
$アホになってる予想 = 'アホになった';
$this->assertEquals($アホになってる予想, $Aさん->足し算して答えが3の倍数ならアホになる(1, 2));
}
}
やった!電卓に依存せずにAさんのテストができた!
電卓はスタブのため、必ず設定した値を返します。
偽の電卓ですね。
電卓のバグの影響を受けずにAさんのテストができました。
じゃあモックは?
このページの上で「テスト対象が依存している他のクラスのメソッドを、期待通り呼び出しているか検査するためのもの」と書きました。
今回のAさんの例で例えると「Aさんが依存している計算できるなにかの足し算メソッドを、期待通り呼び出しているか検査する」となります。
Aさんって本当に「計算できるなにか」を正しく想定通り使ってるの?
を確かめるテストになります。
Aさんが本当に電卓を正しく想定どおり使ってるか確かめる
class Test_Aさん extends TestCase
{
public function test_Aさんは計算できるなにかを正しく使えているのかな()
{
$電卓 = Mockery::mock(計算できるなにか::class);
// 足し算するメソッドが、引数が1と2で、1回だけ実行されるかな?
$電卓->shouldReceive('足し算する')->with(1, 2)->once()->andReturn(3);
$Aさん = new Aさん($電卓);
$Aさん->足し算して答えが3の倍数ならアホになる(1, 2);
}
public function tearDown()
{
Mockery::close();
}
}
Aさんは正しく電卓を使えていた!
Aさんが電卓を正しく想定どおり使っていることを確認できました。
(上のテストケースは、検査対象のメソッドが想定された引数で想定された回数実行されなかったらテストが失敗します)
モックとスタブはどのように使い分ければいいのか
これらの使い分けの観点は、Aさんをテストするときに何に着目しているかだと思います。
Aさんが3の倍数のときにアホになるかどうか確かめたい。でも依存している他のクラスの影響を受けたくない。そんなときはスタブ。
Aさんが依存している計算できるなにかを正しく使えているか確かめたい。依存しているオブジェクトが想定通りの値を受け取って想定回数呼ばれてることを確かめたい。そんなときはモック。
こんな感じかと。
最後に
私個人は、モックは外部が関係する部分でよく使います。
メール送信とか、外部API通信とかですね。
テスト実行のたびにメールが実際に送信されたら面倒だし。
モックとスタブを正しく使い分けて、よいテストを書いていきたいものですね。
(ここで書いたテストケースはコピペで動きます。tearDownにMockery::close()を書いておいてください)