在 PHPUnit 中測試 protected 和 private 方法
前言
就理論上沒有理由要針對已封裝的方法進行測試,對於那些 protected
和 private
方法一般會採取間接的測試方法。意即透過測試 public
方法來驗證裡面所呼叫的封裝方法是否符合預期邏輯,而對外部使用者來說,針對 public
方法的單元測試便會順便包含到這些封裝方法的測試,封裝方法本身就是 public
方法的一部分,他們也不需要去分辨哪些是封裝過的邏輯,因為只需要關注 public
方法即可。
封裝本身代表著:針對外部使用者本來就不需要了解,也根本不了解的成員或方法進行隱藏。單元測試本身就是模擬外部使用者的動作,那當然也只需要對測試目標 public
的方法進行模擬與驗證。
如果你發現測試 public
方法無法協助你測到所有想測試的封裝方法,大部分代表在設計本身上就有問題或是那些 protected
和 private
方法在現階段是沒有必要的,屬於 over design
。
這其實也是 TDD
提倡的精神,如果全部的測試都符合預期,就代表功能完成了。而且按照測試來撰寫的程式,幾乎不會出現測試無法涵蓋的程式,因為程式本來就是為了滿足測試而撰寫的。不需要存在用不到的程式,因此可以避免 over design
。
如何測試封裝方法
我們假設今天有一個 Seafood 的 Class,並且在其中含有一個封裝屬性及封裝方法:
class Seafood
{
protected $foo = 'bar';
private function method()
{
return 'Haha';
}
}
那我們該如何測試中取得被封裝過的屬性和方法呢?
利用 Closure 和 BindTo
public function testProtectedProperty()
{
$seafood = new Seafood();
$foo = function () {
return $this->foo;
};
$bar = $foo->bindTo($seafood, $seafood);
$this->assertEquals('bar', $bar());
}
public function testProtectedMethod()
{
$seafood = new Seafood();
$foo = function () {
return $this->method();
};
$bar = $foo->bindTo($seafood, $seafood);
$this->assertEquals('Haha', $bar());
}
bindTo()
是 Closure 中內建的方法,它的目的就是讓我們能手動注入一個物件,替換掉 Closure 物件中的 $this
,換成我們所注入的物件。
這樣在 Closure 中存取 $foo
和 method
就如同 Seafood
本身去存取自己的成員與方法,就能突破封裝後外部無法存取的限制了。
bindTo() 須在 PHP 5.4 版本以上才有支援。
透過 Reflection
public function testProtectedProperty()
{
$seafood = new Seafood();
$reflector = new ReflectionProperty(Seafood::class, 'foo');
$reflector->setAccessible(true);
$this->assertEquals('bar', $reflector->getValue($seafood));
}
public function testProtectedMethod()
{
$seafood = new Seafood();
$reflector = new ReflectionMethod(Seafood::class, 'method');
$reflector->setAccessible(true);
$this->assertEquals('Haha', $reflector->invoke($seafood));
}
Reflection
能夠讓你針對 class
, interface
, trait
, property
, method
進行檢測,在 Laravel 原始碼中也大量應用了 Reflection
的機制來達成許多黑魔法,例如:Container
。
透過 setAccessible(true)
可以將原本封裝的成員或方法強制變成外界能夠存取的狀態,進而達成我們的目的。
Reflection 須在 PHP 5.3.2 以上才有支援。