本記事はハンズラボAdvent Calendar 2021の17日目です。
はじめに
下記のようなクラスのテストを書きたいなーと思いました。
class Hoge
{
private $clinet; // 外部に依存
private function someMethod()
{
$return = $this->clinet->methodA(); // 外部に依存
$result = Fuga::staticMethod(); // 外部に依存
// なんかの処理
}
public function callSomeMethod()
{
// なんかの処理(長い&外部依存あり)
$this->someMethod();
// なんかの処理(長い&外部依存あり)
}
// $clinetの初期化処理など
// 他なんかの処理
}
テストコードの中でインスタンス化して、public関数を呼んで、返り値なりなんなりを評価すればよいのですが、実行環境が揃ってないと外部依存箇所でエラーになりますよね。
# テストコード
pubic function testCallSomeMethod()
{
$hoge = new Hoge();
$return = $hoge->callSomeMethod(); // 外部リソースに依存するためエラー
// assertion
}
そんなときは固定値を返すハリボテ(モック)を作って、外部依存箇所が呼ばれるとハリボテが動作するようにすればよいです。
…なのですが、public関数の処理が長かったり、複数の外部依存関係があった場合、モックの設定などが面倒です。
また、テストコード作成にかける時間がたくさんあれば良いですが、そんな時間はありません。
privateメソッドのテストを書く・書かない議論はありますが、限られた時間の中である程度の品質を担保するために、privateメソッドのテストを書くことにしました。
前提
- PHP 7.3
- PHPUnit 8.5.8
- Mockery 1.4.4
テストコード
基本はPHPUnitの機能を使います。手の届かないところをMockeryでやります。
privateメソッドのテスト方法
privateメソッドはPHPUnitでもMockeryでもテストする機構がないので、リフレクションを使ってコールします。
// Hogeクラスのインスタンスからリフレクションを取得し、someMethodを外から呼べるようにします
$hoge = new Hoge();
$reflection = new ReflectionClass($hoge);
$method = $reflection->getMethod("someMethod");
$method->setAccessible(true);
// someMethodをコール
$method->invoke($hoge)
// 引数がある場合
$method->invoke($hoge, 1, 2, 3)
外部依存プロパティの置換
プロパティの$clinetは外部リソースに依存しており、これを呼ぶとエラーになるのでモックを作って置き換えます。
モックの作成
PHPUnitのgetMockBuilderを使ってモックを作ります。
// HogeClientクラスのモック
$mock = $this->getMockBuilder(HogeClient::class)
->setConstructorArgs([""]) // コンストラクタの引数あれば
->onlyMethods(["methodA"]) // モックとして動作を置き換えるメソッド
->getMock();
// モックの設定
// methodAが1回呼ばれて、1を返す設定
$mock->expects($this->once())
->method("methodA")
->will($this->returnValue("1"));
プロパティの置換
作ったモックを既存プロパティと置き換えます。
// $hoge = new Hoge();
// $reflection = new ReflectionClass($hoge);
// プロパティclientをモックに置き換え
$client = $reflection->getProperty('client');
$client->setAccessible(true);
$client->setValue($hoge, $mock);
これで、someMethodをinvokeすると、$this->clinet->methodA()
はエラーにならず、1を返してくれます。
メソッドの置換
次に、外部に依存するFuga::staticMethod()
をなんとかします。
テストコード中の特定の処理をモックにしたいなーというときは、Mockeryのoverloadを使います。
// Fugaクラスのモック作成
$haribote = \Mockery::mock('overload:Fuga');
// モック設定
// staticMethodが1回呼ばれて、0を返す設定
$haribote->shouldReceive('staticMethod')->once()->andReturn(0);
これで、テストコード中でFuga::staticMethod()
が呼ばれると0を返してくれます。
コードまとめ
今までのコードをまとめるとこんな感じです。
外部に依存するところはモックが動作するので、エラーとならずにテストができました。
<?php
use PHPUnit\Framework\TestCase;
use ReflectionClass;
class HogeTest extends TestCase
{
public function testSomeMethod()
{
// Hogeクラスのインスタンスからリフレクションを取得し、someMethodを外から呼べるようにします
$hoge = new Hoge();
$reflection = new ReflectionClass($hoge);
$method = $reflection->getMethod("someMethod");
$method->setAccessible(true);
// HogeClientクラスのモック
$mock = $this->getMockBuilder(HogeClient::class)
->setConstructorArgs([""]) // コンストラクタの引数あれば
->onlyMethods(["methodA"]) // モックとして動作を置き換えるメソッド
->getMock();
// モックの設定
// methodAが1回呼ばれて、1を返す設定
$mock->expects($this->once())
->method("methodA")
->will($this->returnValue("1"));
// プロパティclientをモックに置き換え
$client = $reflection->getProperty('client');
$client->setAccessible(true);
$client->setValue($hoge, $mock);
// Fugaクラスのモック作成
$haribote = \Mockery::mock('overload:Fuga');
// モック設定
// staticMethodが1回呼ばれて、0を返す設定
$haribote->shouldReceive('staticMethod')->once()->andReturn(0);
// someMethodをコール
$res = $method->invoke($hoge)
// モックのクリア
\Mockery::close();
// assertion
}
}
さいごに
多分初めてテスト書いたのですが、単体テストって大事だなーと思いました。
安心感が違います。