Szukaj…


Dostęp do prywatnych i chronionych zmiennych członkowskich

Odbicie jest często używane w ramach testowania oprogramowania, na przykład do tworzenia / tworzenia instancji wykonawczych obiektów próbnych. Świetnie nadaje się również do sprawdzania stanu obiektu w dowolnym momencie. Oto przykład użycia Refleksji w teście jednostkowym do sprawdzenia, czy chroniony element klasy zawiera oczekiwaną wartość.

Poniżej znajduje się bardzo podstawowa klasa dla samochodu. Posiada chronioną zmienną składową, która będzie zawierać wartość reprezentującą kolor samochodu. Ponieważ zmienna składowa jest chroniona, nie możemy uzyskać do niej bezpośredniego dostępu i musimy użyć metody pobierającej i ustawiającej, aby odpowiednio pobrać i ustawić jej wartość.

class Car
{
    protected $color
    
    public function setColor($color)
    {
        $this->color = $color;
    }
    
    public function getColor($color)
    {
        return $this->color;
    }
}

Aby to przetestować, wielu programistów utworzy obiekt Car, ustawi kolor Car::setColor() za pomocą Car::setColor() , pobierze kolor za pomocą Car::getColor() i porówna tę wartość z ustawionym kolorem:

/**
 * @test
 * @covers     \Car::setColor
 */
public function testSetColor()
{
    $color = 'Red';

    $car = new \Car();
    $car->setColor($color);
    $getColor = $car->getColor();
        
    $this->assertEquals($color, $reflectionColor);
}

Na pierwszy rzut oka wydaje się to w porządku. W końcu wszystko, co Car::getColor() , zwraca wartość chronionej zmiennej członkowskiej Car::$color . Ale ten test jest wadliwy na dwa sposoby:

  1. Wykonuje Car::getColor() co jest poza zakresem tego testu
  2. Zależy to od Car::getColor() który może mieć sam błąd, który może spowodować, że test będzie fałszywie dodatni lub ujemny

Zobaczmy, dlaczego nie powinniśmy używać Car::getColor() w naszym teście jednostkowym i zamiast tego powinniśmy używać Reflection. Powiedzmy, że programista ma za zadanie dodać „Metaliczny” do każdego koloru samochodu. Car::getColor() więc zmodyfikować Car::getColor() aby Car::getColor() „Metallic” do koloru samochodu:

class Car
{
    protected $color
    
    public function setColor($color)
    {
        $this->color = $color;
    }
    
    public function getColor($color)
    {
        return "Metallic "; $this->color;
    }
}

Czy widzisz błąd? Deweloper użył średnika zamiast operatora konkatenacji, próbując nadać „Metallic” kolor samochodu. W rezultacie za każdym razem, gdy Car::getColor() jest Car::getColor() jest „Metaliczne” bez względu na faktyczny kolor samochodu. W rezultacie nasz test jednostkowy Car::setColor() zakończy się niepowodzeniem, mimo że Car::setColor() działa idealnie dobrze i ta zmiana nie miała na niego wpływu .

Jak więc zweryfikować, że Car::$color zawiera wartość, którą ustawiamy za pomocą Car::setColor() ? Możemy użyć Refelection do bezpośredniej kontroli chronionej zmiennej członka. Więc jak to zrobić? Możemy użyć Refelection, aby chroniona zmienna członka była dostępna dla naszego kodu, aby mógł pobrać wartość.

Najpierw zobaczmy kod, a następnie go podzielimy:

/**
 * @test
 * @covers     \Car::setColor
 */
public function testSetColor()
{
    $color = 'Red';

    $car = new \Car();
    $car->setColor($color);
    
    $reflectionOfCar = new \ReflectionObject($car);
    $protectedColor = $reflectionOfForm->getProperty('color');
    $protectedColor->setAccessible(true);
    $reflectionColor = $protectedColor->getValue($car);
    
    $this->assertEquals($color, $reflectionColor);
}

Oto jak używamy Reflection, aby uzyskać wartość Car::$color w powyższym kodzie:

  1. Tworzymy nowy ReflectionObject reprezentujący nasz obiekt Car
  2. Otrzymujemy właściwość ReflectionProperty dla Car::$color (to „reprezentuje” zmienną Car::$color )
  3. Sprawiamy, że Car::$color dostępny w Car::$color
  4. Otrzymujemy wartość Car::$color

Jak widać za pomocą Reflection możemy uzyskać wartość Car::$color bez konieczności wywoływania Car::getColor() lub jakiejkolwiek innej funkcji akcesorium, która może powodować nieprawidłowe wyniki testu. Teraz nasz test jednostkowy Car::setColor() jest bezpieczny i dokładny.

Wykrywanie funkcji klas lub obiektów

Wykrywanie cech klas można częściowo wykonać za pomocą funkcji property_exists i method_exists .

class MyClass {
    public $public_field;
    protected $protected_field;
    private $private_field;
    static $static_field;
    const CONSTANT = 0;
    public function public_function() {}
    protected function protected_function() {}
    private function private_function() {}
    static function static_function() {}
}

// check properties
$check = property_exists('MyClass', 'public_field');    // true
$check = property_exists('MyClass', 'protected_field'); // true
$check = property_exists('MyClass', 'private_field');   // true, as of PHP 5.3.0
$check = property_exists('MyClass', 'static_field');    // true
$check = property_exists('MyClass', 'other_field');     // false

// check methods
$check = method_exists('MyClass', 'public_function');    // true
$check = method_exists('MyClass', 'protected_function');    // true
$check = method_exists('MyClass', 'private_function');    // true
$check = method_exists('MyClass', 'static_function');    // true

// however...
$check = property_exists('MyClass', 'CONSTANT');  // false
$check = property_exists($object, 'CONSTANT');    // false

Za pomocą ReflectionClass można również wykryć stałe:

$r = new ReflectionClass('MyClass');
$check = $r->hasProperty('public_field');  // true
$check = $r->hasMethod('public_function'); // true
$check = $r->hasConstant('CONSTANT');      // true
// also works for protected, private and/or static members.

Uwaga: w przypadku property_exists i method_exists , zamiast nazwy klasy można podać również obiekt klasy zainteresowania. Za pomocą refleksji należy użyć klasy ReflectionObject zamiast ReflectionClass .

Testowanie metod prywatnych / chronionych

Czasami warto przetestować metody prywatne i chronione, a także metody publiczne.

class Car
{
    /**
     * @param mixed $argument
     *
     * @return mixed
     */
    protected function drive($argument)
    {
        return $argument;
    }

    /**
     * @return bool
     */
    private static function stop()
    {
        return true;
    }
}

Najprostszym sposobem na przetestowanie metody jazdy jest użycie odbicia

class DriveTest
{
    /**
     * @test
     */
    public function testDrive()
    {
        // prepare
        $argument = 1;
        $expected = $argument;
        $car = new \Car();

        $reflection = new ReflectionClass(\Car::class);
        $method = $reflection->getMethod('drive');
        $method->setAccessible(true);

        // invoke logic
        $result = $method->invokeArgs($car, [$argument]);

        // test
        $this->assertEquals($expected, $result);
    }
}

Jeśli metoda jest statyczna, zamiast instancji klasy przekazujesz null

class StopTest
{
    /**
     * @test
     */
    public function testStop()
    {
        // prepare
        $expected = true;

        $reflection = new ReflectionClass(\Car::class);
        $method = $reflection->getMethod('stop');
        $method->setAccessible(true);

        // invoke logic
        $result = $method->invoke(null);

        // test
        $this->assertEquals($expected, $result);
    }
}


Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow