ユニットテストを書く上でphpunitのモック、スタブ化についての挙動をまとめます。
環境
- php 7.1
- phpunit 7.0
- phake 3.1.6
まとめ
それぞれスタブ化したメソッドを直接・間接的に呼んだ場合の表です。
○:想定どおりスタブ化されている
☓:スタブ化されない
privateメソッドを間接的に呼んでいるメソッドのテストを書きたい場合は、
クラス設計の見直しか、protectedに変更する方法があります。
public | private | protected | 別クラスのpublic | |
---|---|---|---|---|
直接呼ぶ | ○ | ○ | ○ | ☓(呼べない) |
間接的に呼ぶ | ○ | ☓ | ○ | ○ |
1.モック化しないでテストする
この場合実際のソースを使ってテストすることになる。
<?
class Foo {
public function functionA () {
return 'hoge';
}
public function functionB () {
return 'fuga';
}
}
<?
use PHPUnit\Framework\TestCase;
require_once("Foo.php");
class FooTest extends TestCase{
public function test_mockを使わない () {
$target = new Foo();
// 実際のソースを使ってテストする
$result = $target->functionA();
$this->assertSame('hoge', $result);
$result = $target->functionB();
$this->assertSame('fuga', $result);
}
2. mockを使ってテストする
Phake::mockで簡単にモックを作成することができます。
作成したモックをPhake::whenで振る舞いを定義し、スタブ化することもできます。
スタブ化していない関数はすべてモック化され、nullが返ります。
public function functionA () {
return 'hoge';
}
public function functionB () {
return 'fuga';
}
public function test_mockを使う () {
$mock = Phake::mock('Foo');
// 振る舞いを定義していない関数は、nullになる
$result = $mock->functionA();
$this->assertSame(null, $result);
$result = $mock->functionB();
$this->assertSame(null, $result);
// 振る舞いを定義した関数は、スタブ化できる
Phake::when($mock)->functionA->thenReturn('HOGE');
$result = $mock->functionA();
$this->assertSame('HOGE', $result);
// functionBは振る舞いを定義していないのでnullになる
$result = $mock->functionB();
$this->assertSame(null, $result);
}
3. partialMockを使ってテストする
Phake::partialMockで簡単にpartialモックを作成することができます。
作成したpartialモックをPhake::whenで振る舞いを定義し、スタブ化することもできます。
partialモックの場合、スタブ化していない関数はすべて実際のソースと同じ挙動をします。
public function functionA () {
return 'hoge';
}
public function functionB () {
return 'fuga';
}
public function test_partialMockを使う () {
$pmock = Phake::partialMock('Foo');
// 振る舞いを定義していない関数は、元の関数の振る舞いをする
$result = $pmock->functionA();
$this->assertSame('hoge', $result);
$result = $pmock->functionB();
$this->assertSame('fuga', $result);
// 振る舞いを定義した関数は、スタブ化できる
Phake::when($pmock)->functionA->thenReturn('HOGE');
$result = $pmock->functionA();
$this->assertSame('HOGE', $result);
// functionBは振る舞いを定義していないので本来の戻り値になる
$result = $pmock->functionB();
$this->assertSame('fuga', $result);
}
4. スタブ化したpublicメソッドを直接呼んでテストする
スタブにしたpublicメソッドは、指定した返り値を返す。
元のメソッドが関数エラーやExceptionを起こすものだとしても、テスト時には発生しない。
public function functionC ($flg) {
if ($flg) {
undefinedFunction(); // 未定義のファンクションなのでエラーが発生する
} else {
throw new exception('throw exception!!');
}
return 'hoge';
}
public function test_public_methodのスタブを直接呼ぶ () {
$pmock = Phake::partialMock('Foo');
Phake::when($pmock)->functionC(Phake::anyParameters())->thenReturn('HOGE');
// 未定義のファンクションエラーもexceptionも発生しない
$result = $pmock->functionC(true);
$this->assertSame('HOGE', $result);
$result = $pmock->functionC(false);
$this->assertSame('HOGE', $result);
}
5. スタブ化したpublicメソッドを間接的に呼んでテストする
この場合でも4の場合と同じく、
元のメソッドが関数エラーやExceptionを起こすものだとしても、テスト時には発生しない。
public function functionC ($flg) {
if ($flg) {
undefinedFunction();
} else {
throw new exception('throw exception!!');
}
return 'hoge';
}
public function functionD ($flg) {
return $this->functionC($flg);
}
public function test_public_methodのスタブを間接的に呼ぶ () {
$pmock = Phake::partialMock('Foo');
Phake::when($pmock)->functionC(Phake::anyParameters())->thenReturn('HOGE');
// 未定義のファンクションエラーもexceptionも発生しない
$result = $pmock->functionD(true);
$this->assertSame('HOGE', $result);
$result = $pmock->functionD(false);
$this->assertSame('HOGE', $result);
}
6. スタブ化したprivateメソッドを直接呼んでテストする
phakeではprivateもpublicメソッドと同様にPhake::whenでスタブ化することができる。
実行する際は、Phake::makeVisible($mock)を使用する。
この場合、publicと同様で
元のメソッドが関数エラーやExceptionを起こすものだとしても、テスト時には発生しない。
private function functionE ($flg) {
if ($flg) {
undefinedFunction();
} else {
throw new exception('throw exception!!');
}
return 'hoge';
}
public function test_private_methodのスタブを直接呼ぶ () {
$pmock = Phake::partialMock('Foo');
// private methodをmockにする方法はpublicと同じ
Phake::when($pmock)->functionE(Phake::anyParameters())->thenReturn('HOGE');
// ただし、publicと同じように呼ぶことはできない
// $result = $pmock->functionE(true);
// makeVisibleを使えばprivateメソッドも実行できる
// 未定義のファンクションエラーもexceptionも発生しない
$result = Phake::makeVisible($pmock)->functionE(true);
$this->assertSame('HOGE', $result);
$result = Phake::makeVisible($pmock)->functionE(false);
$this->assertSame('HOGE', $result);
}
7. スタブ化したprivateメソッドを間接的に呼んでテストする
ここが重要です。
privateメソッドもPhake::whenでスタブ化し、Phake::makeVisibleで実行することができました。
しかし、そのスタブ化したprivateメソッドを別のファンクションから実行した場合、スタブ化されません。
ここではpartialモックを使っているので、対象のprivateメソッドは実際のソースと同じ挙動をします。
private function functionE ($flg) {
if ($flg) {
undefinedFunction();
} else {
throw new exception('throw exception!!');
}
return 'hoge';
}
public function functionF ($flg) {
return $this->functionE($flg);
}
public function test_private_methodのスタブを関節的に呼ぶ () {
$pmock = Phake::partialMock('Foo');
// private methodをmockにする方法はpublicと同じ
Phake::when($pmock)->functionE(Phake::anyParameters())->thenReturn('HOGE');
// 未定義のファンクションエラーが発生する(スタブ化できてない)
// Error: Call to undefined function undefinedFunction()
// $result = Phake::makeVisible($pmock)->functionF(true);
// $this->assertSame('HOGE', $result);
// exceptionが発生する(スタブ化できてない)
try {
$result = Phake::makeVisible($pmock)->functionF(false);
} catch (Exception $e) {
$result = $e->getMessage();
}
$this->assertSame('throw exception!!', $result);
}
8. 別クラスのpublicメソッドを間接的に呼んでテストする(7.解決策として)
そもそもprivateメソッドのテストを書くべきではないし、
書かなければいけないということは、privateメソッドの責務を超えているのではないのか。
そのprivateメソッドは別クラスのpublicクラスではないのか、という考えのもと、先程のprivateメソッドを別クラスのpublicメソッドとして実装し、テストします。
(privateメソッドはテストすべきではない、という点の詳細は今回は割愛します)
この場合、スタブ化したpublicメソッドを呼んでいることになるので、5のパターンと同様に、想定通り定義した挙動をします。
require_once("Bar.php");
class Foo {
// メソッドの引数にオブジェクトを渡せるようにして、テスト時に依存性を注入できるようにする
public function functionG (Bar $Bar, $flg) {
// BarクラスのEを呼びます
return $Bar->functionE($flg);
}
}
<?
class Bar {
public function functionE ($flg) {
if ($flg) {
undefinedFunction();
} else {
throw new exception('throw exception!!');
}
return 'hoge';
}
}
public function test_別クラスのpublicをスタブ化してテストする () {
$barPmock = Phake::partialMock('Bar');
$fooPmock = Phake::partialMock('Foo');
Phake::when($barPmock)->functionE(Phake::anyParameters())->thenReturn('HOGE');
// スタブ化したpublicメソッドを間接的に読んでいるので振る舞いどおりに動く
$result = $fooPmock->functionG($barPmock, false);
$this->assertSame('HOGE', $result);
}
9. スタブ化したprotectedメソッドを直接呼んでテストする
6のスタブ化したprivateメソッドを直接呼んでテストする場合と同じ挙動をします。
実行する際は、Phake::makeVisible($mock)を使用します。
protected function functionH ($flg) {
if ($flg) {
undefinedFunction();
} else {
throw new exception('throw exception!!');
}
return 'hoge';
}
public function test_protected_methodのスタブを直接呼ぶ () {
$pmock = Phake::partialMock('Foo');
// private methodをmockにする方法はpublicと同じ
Phake::when($pmock)->functionH(Phake::anyParameters())->thenReturn('HOGE');
// ただし、publicと同じように呼ぶことはできない
// $result = $pmock->functionH(true);
// makeVisibleを使えばprotectedメソッドも実行できる(privateと同じ)
// 未定義のファンクションエラーもexceptionも発生しない
$result = Phake::makeVisible($pmock)->functionH(true);
$this->assertSame('HOGE', $result);
$result = Phake::makeVisible($pmock)->functionH(false);
$this->assertSame('HOGE', $result);
}
10. スタブ化したprotectedメソッドを間接的に呼んでテストする
ここが重要です。
スタブ化したprivateメソッドは間接的に呼ぶと、スタブとして機能しませんが、
protectedの場合は、スタブとして機能します。
どうしてもprivateメソッドのテストを書きたく、他のクラスのpublicメソッドにするのに抵抗がある場合はこちらの方法がコスパ良いかもしれません。
protected function functionH ($flg) {
if ($flg) {
undefinedFunction();
} else {
throw new exception('throw exception!!');
}
return 'hoge';
}
public function functionI ($flg) {
return $this->functionH($flg);
}
public function test_protected_methodのスタブを間接的に呼ぶ () {
$pmock = Phake::partialMock('Foo');
// private methodをmockにする方法はpublicと同じ
Phake::when($pmock)->functionH(Phake::anyParameters())->thenReturn('HOGE');
// 未定義のファンクションエラーもexceptionも発生しない
$result = $pmock->functionI(true);
$this->assertSame('HOGE', $result);
$result = $pmock->functionI(false);
$this->assertSame('HOGE', $result);
}
まとめ(再度)
それぞれスタブ化したメソッドを直接・間接的に呼んだ場合の表です。
○:想定どおりスタブ化されている
☓:スタブ化されない
privateメソッドを間接的に呼んでいるメソッドのテストを書きたい場合は、
クラス設計の見直しか、protectedに変更する方法があります。
public | private | protected | 別クラスのpublic | |
---|---|---|---|---|
直接呼ぶ | ○ | ○ | ○ | ☓(呼べない) |
間接的に呼ぶ | ○ | ☓ | ○ | ○ |
参考
PHPのモッキンフレームワークPhakeの使い方
https://taisablog.com/archives/213