PHP
Odbicie
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:
- Wykonuje
Car::getColor()
co jest poza zakresem tego testu - 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:
- Tworzymy nowy ReflectionObject reprezentujący nasz obiekt Car
- Otrzymujemy właściwość ReflectionProperty dla
Car::$color
(to „reprezentuje” zmiennąCar::$color
) - Sprawiamy, że
Car::$color
dostępny wCar::$color
- 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);
}
}