在 PHPUnit 中測試 protected 和 private 方法

前言

就理論上沒有理由要針對已封裝的方法進行測試,對於那些 protectedprivate 方法一般會採取間接的測試方法。意即透過測試 public 方法來驗證裡面所呼叫的封裝方法是否符合預期邏輯,而對外部使用者來說,針對 public 方法的單元測試便會順便包含到這些封裝方法的測試,封裝方法本身就是 public 方法的一部分,他們也不需要去分辨哪些是封裝過的邏輯,因為只需要關注 public 方法即可。

封裝本身代表著:針對外部使用者本來就不需要了解,也根本不了解的成員或方法進行隱藏。單元測試本身就是模擬外部使用者的動作,那當然也只需要對測試目標 public 的方法進行模擬與驗證。

如果你發現測試 public 方法無法協助你測到所有想測試的封裝方法,大部分代表在設計本身上就有問題或是那些 protectedprivate 方法在現階段是沒有必要的,屬於 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 中存取 $foomethod 就如同 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 以上才有支援。