変更できないサードパーティクラスを拡張する必要があります。クラスの依存関係は、ほとんどの部分がコンストラクターを通じて注入されるため、簡単にモック化できます。ただし、クラスはグローバルサービスを直接呼び出すトレイトのメソッドも使用し、テストの分離を破壊します。問題を示すコードサンプルを次に示します。
trait SomeTrait {
public function irrelevantTraitMethod() {
throw new \Exception('Some undesirable behavior.');
}
}
class SomeSystemUnderTest {
use SomeTrait;
public function methodToTest($input) {
$this->irrelevantTraitMethod();
return $input;
}
}
class MockWithTraitTest extends \PHPUnit_Framework_TestCase {
public function testMethodToTest() {
$sut = new SomeSystemUnderTest();
$this->assertSame(123, $sut->methodToTest(123)); // Unexpected exception!
}
}
PHPUnitでこのような状況に対処するにはどうすればよいですか?トレイトをどうにかして模倣して注入できますか?私が見つけた唯一の解決策は、SUT自体をモックし、特性から問題のあるメソッドを無効にし、実際のSUTメソッドはそのままにしておくことです。しかし、それは正しくありません。
誰かがより良い答えをいつか提供することを願っていますが、当面-他の誰かが同じ問題を扱っている場合に備えて-私が質問で言及した回避策は次のとおりです。同じ特性とSUTが与えられた場合、以下のテストはパスします。 SUTの「部分的なモック」を作成し、traitメソッドのみを置き換え、残りはそのままにして行使およびアサートします。
class MockWithTraitTest extends \PHPUnit_Framework_TestCase {
public function testMethodToTest() {
/** @var \SomeSystemUnderTest|\PHPUnit_Framework_MockObject_MockObject $sut */
$sut = $this->getMockBuilder('\SomeSystemUnderTest')
->setMethods(array('irrelevantTraitMethod'))
->getMock();
$this->assertSame(123, $sut->methodToTest(123)); // OK.
}
}
このアプローチは機能しますが、実装との緊密な結合が必要であり、脆弱なテストにつながる恐れがあるため、私はそれが嫌いです。 (たとえば、ロギング用に別のメソッドをトレイトに追加して、SUTで使用するとどうなりますか?すべてのモックを更新するまで、無関係なテストが失敗します。)トレイト全体をモックできるようにしたいと思います。
あなたが望むことをする方法はありません。トレイトは、クラスやパブリックAPIを定義する方法であり、インターフェースや継承と同じです。依存関係をあざけるようにそれを「あざける」ことはできません。
何が機能するか(試してみてください):リフレクションを使用して、特性が定義するすべてのメソッドを動的に「無効」にします。
しかし、これには別の問題があります。実装クラスは、トレイトのメソッドを上書きするか、同じ名前の別のトレイトのメソッドを使用できます。 「ターゲット」特性が定義するすべてのメソッドを単に「無効にする」だけでは、完全にうまくいくわけではありません。
運が良ければ、リフレクションはそれを回避するのにも役立ちます。