Testy w PHP - Mock czyli Test Double post
2018-10-05
W języku polskim słowo „imitacja” chyba najlepiej oddaje czym jest „mock”. Coś co tylko naśladuje oryginalny byt, zachowuje się w bardzo podobny do niego sposób jednak nim nie jest. Mówiąc, że mamy coś „zamockować” jako programiści tak naprawdę myślimy o stworzeniu implementacji interfejsu która naśladuje prawdziwą.
Test Double, tak w zasadzie powinniśmy nazywać to co w praktyce nazywane jest Mockiem.
Przyjęło się, że właśnie tak
nazywamy zbiór wzorców wspomagających automatyczne testowanie kodu, kiedy Mock jest jedynie jednym jednym typem Test Double
.
No dobra, ale po co imitować cokolwiek w kodzie? W kodzie produkcyjnym po nic, przeznaczeniem Test Double
jest
usprawnienie i uproszczenie procesu automatycznego testowania oprogramowania.
Załóżmy przez chwilę, że jesteś inżynierem, który pracuje dla jakiegoś wielkiego koncernu motoryzacyjnego i programujesz dla nich tempomatem. Tempomat to mechanizm pozwalający jechać ze stałą ustaloną prędkością niezależnie od tego czy pojazd porusza się po płaskiej powierzchni czy też pochyłej. Nie musisz więc dodawać gazu kiedy auto jedzie pod górkę, tempomat sam zwiększy obroty w celu zachowania stałej prędkości. Upraszczając logika jaka za tym stoi mówi „jeżeli prędkość spada, podnoś obroty do momentu przywrócenia oczekiwanej prędkości”. Programowanie tego mechanizmu nie jest najtrudniejsze, o wiele trudniej jest napisać do niego testy. O ile logika tempomatu (tego uproszczonego opisanego przeze mnie) jest prosta tak pewnie nie tak łatwo będzie za każdym razem w testach odpalać silnik, zmniejszać i zwiększać opór jaki na niego działa tylko po to żeby sprawdzić czy kod tempomatu który napisaliśmy robi co należy, nie? No nie, nikt też nie będzie rozpędzał za każdą zmianą w kodzie pojazdu do określonej prędkości aby czujnik pomiaru prędkości zwrócił odpowiednie wartości o które opierać się będzie nasz mechanizm dlatego właśnie w celu przetestowania tempomatu posłużymy się tylko imitacją silnika czy też czujnika, ich nieprawdziwymi implementacjami stworzonymi wyłącznie na potrzeby testów tempomatu.
namespace Example\Car;
final class CruisingControlSystem
{
private $expectedSpeed;
public function __construct(Speed $expectedSpeed)
{
$this->expectedSpeed = $expectedSpeed;
}
public function control(SpeedSensor $speedSensor, Engine $engine) : void
{
if ($speedSensor->current()->isZero()) {
throw new \RuntimeException('Car is stopped, can\'t set the speed.');
}
if ($speedSensor->current()->lowerThan($this->expectedSpeed)) {
$engine->speedUp();
}
if ($speedSensor->current()->greaterThan($this->expectedSpeed)) {
$engine->speedDown();
}
}
}
Tak wiem, to wybitnie naiwna i słaba implementacja tempomatu ale ma służyć tylko do zobrazowania Test Doubles
, nie znam się na
silnikach, tempomatach, czujnikach prędkości - tak sobie to tylko w uproszczony sposób wyobraziłem.
Wiemy już dlaczego, pozostaje zadać pytanie „czym są?”.
Mówiąc wprost wyróżniamy 5 typów imitacji czyli inaczej mówiąc 5 typów Test Doubles
.
- Stub
- Spy
- Dummy
- Fake
- Mock
Tak tak, Mock to po prostu typ Test Double
który jednak przyjął się w środowisku bardzo mocno.
Wzięło się to jak donoszą różne źródła po części z niewiedzy (ludzie nie rozumieli dokładnie czym są Mocki) oraz z faktu dobrego brzmienia tego słowa.
Zajmijmy się jednak opisaniem przeznaczenia każdego z wymienionych wyżej 5 typów.
Stub
Imitacja mająca jasno określony cel. Jest to nic innego jak możliwie najprostsza implementacja danego interfejsu, która
w zasadzie na nic nie reaguje ponieważ jej zachowanie jest z góry zdefiniowane. Odnosząc się do naszego przykładu dobrym
Stubem
byłaby implementacja czujnika prędkości zwracająca konkretną wartość.
final class ConstantSpeedSensorStub implements SpeedSensor
{
private $speed;
public function __construct(Speed $speed)
{
$this->speed = $speed;
}
public function current(): Speed
{
return $this->speed;
}
}
To tyle, nie ma tu więcej definicji. Stub
to konkretna implementacja danego interfejsu posiadająca z góry określone,
czasami wręcz zapisane na sztywno zachowanie.
Spy
Imitacja której celem jest nie tyle zrobienie czegoś konkretnego co sprawdzenie czy coś zostało zrobione ale bez
definiowania z góry naszych oczekiwań. Spy
używamy w celu zweryfikowania jakiegoś zachowania.
final class EngineSpeedSpy implements Engine
{
public $speedIncreased;
public $speedDecreased;
public function __construct()
{
$this->speedDecreased = false;
$this->speedIncreased = false;
}
public function speedUp(): void
{
$this->speedIncreased = true;
}
public function speedDown(): void
{
$this->speedDecreased = true;
}
}
Dummy
Najprostsza imitacja która ma za zadanie stać i nic nie robić, dosłownie. Czasami testowany obiekt posiada jakieś
zależności które z perspektywy danego testu nie mają znaczenia.
Do ich wywołania może np. nigdy nie dochodzić ponieważ jakaś asercja wcześniej przerywa działanie metody i rzuca
exception. Nie ma więc powodu tworzyć na tą okazję Stuba ani Mocka, lepiej stworzyć Dummy
.
final class EngineDummy implements Engine
{
public function speedUp(): void
{
}
public function speedDown(): void
{
}
}
Fake
Tworząc fake, tworzymy tak naprawdę imitację bardzo zbliżoną do oryginału, w dalszym stopniu jednak uproszczoną, najczęściej po to aby zasymulować konkretne zachowanie zależne od parametrów wejściowych. Przykład z tempomatem jest zbyt prosty dlatego Fake jest prosty ale w rzeczywistości mogą być one bardzo skomplikowane, ich komplikacja wynika z tego, że posiadają logikę. Uproszczoną ale dalej logikę.
Robiłeś kiedyś implementację In Memory
bazy danych?
To właśnie jest Fake
, zachowanie zbliżone do oryginału jednak uproszczone do niezbędnego minimum.
final class RandomSpeedSensorFake implements SpeedSensor
{
public function current(): Speed
{
do {
$speed = SpeedMother::random();
} while ($speed->isZero());
return $speed;
}
}
Mock
Mocka zostawiłem na koniec ponieważ jest on najbardziej rozbudowanym Test Doublem
. Na pierwszy rzut oka nie różni się
niczym od Spy’a
czy Stuba
jednak w rzeczywistości różni się bardzo. Mock
to tak jakby zaprogramowana imitacja,
której nie tylko przekazujemy nasze oczekiwania ale również mówimy jak się ma zachować. Mocki budujemy na żywo przed
testem podczas gdy Stub
czy Spy
to zwykła klasa. Mocki są tak skomplikowane, że do ich tworzenia najczęściej używa
się specjalne frameworki takie jak Mockery czy PHPUnit Mock Builder.
$speed = SpeedMother::random();
$ccs = new CruisingControlSystem($speed);
$engineMock = $this->createMock(Engine::class);
$engineMock->expects($this->never())
->method('speedUp');
$engineMock->expects($this->never())
->method('speedDown');
$ccs->control(
new ConstantSpeedSensorStub($speed),
$engineMock
);
Wiedząc już czym tak naprawdę są Test Doubles
możemy teraz odpowiedzieć na pytanie „jak używać?”
final class CruisingControlSystemTest extends TestCase
{
public function test_increasing_speed_when_sensor_says_the_current_speed_is_too_low()
{
$speed = SpeedMother::random();
$ccs = new CruisingControlSystem($speed);
$engineSpy = new EngineSpeedSpy();
$ccs->control(
new ConstantSpeedSensorStub($speed->decreaseByTimes(2)),
$engineSpy
);
$this->assertTrue($engineSpy->speedIncreased);
$this->assertFalse($engineSpy->speedDecreased);
}
public function test_decreasing_speed_when_sensor_says_the_current_speed_is_too_high()
{
$speed = SpeedMother::random();
$ccs = new CruisingControlSystem($speed);
$engineSpy = new EngineSpeedSpy();
$ccs->control(
new ConstantSpeedSensorStub($speed->increaseByTimes(2)),
$engineSpy
);
$this->assertFalse($engineSpy->speedIncreased);
$this->assertTrue($engineSpy->speedDecreased);
}
public function test_not_changing_speed_when_sensor_says_the_current_speed_is_fine()
{
$speed = SpeedMother::random();
$ccs = new CruisingControlSystem($speed);
$engineMock = $this->createMock(Engine::class);
$engineMock->expects($this->never())
->method('speedUp');
$engineMock->expects($this->never())
->method('speedDown');
$ccs->control(
new ConstantSpeedSensorStub($speed),
$engineMock
);
}
public function test_controlling_speed_when_speed_sensor_tells_that_car_is_stopped()
{
$ccs = new CruisingControlSystem(SpeedMother::random());
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Car is stopped, can\'t set the speed.');
$ccs->control(
new ConstantSpeedSensorStub(SpeedMother::zero()),
new EngineDummy()
);
}
}
Podsumowując można powiedzieć, że Stub
używamy kiedy musimy jasno określić zachowanie, Spy
kiedy chcemy je sprawdzić,
Dummy
gdy nie zależy nam na jakimkolwiek zachowaniu, Fake
kiedy chcemy otrzymać zachowanie zbliżone do oryginalnego a
Mock
kiedy w każdym teście musimy zaprogramować inne zachowanie w zależności od różnych czynników.
W zasadzie wszystkie Test Double
można zastąpić Mockiem
jednak ma to swoją cenę. Rośnie złożoność testu, zmniejsza
się jego czytelność, zwiększa się podatność na błędy (w końcu to zachowanie trzeba zaprogramować). Jeżeli nie do końca
wiesz czy potrzebujesz Stuba
/Spy’a
czy może jednak Mocka
zacznij od pierwszej opcji. Te test doubles praktycznie
generują się poprzez IDE. Tworzysz plik, implementujesz interfejs, IDE tworzy metody a Ty je uzupełniasz.
Jeżeli musisz tworzyć zbyt wiele Stubów
jednego typu to znak, że może przydałby się Mock.
Autorem pojęcia Test Double
jest Gerard Meszaros.
Sometimes it is just plain hard to test the system under test (SUT) because it depends on other components that cannot be used in the test environment. This could be because they aren't available, they will not return the results needed for the test or because executing them would have undesirable side effects. In other cases, our test strategy requires us to have more control or visibility of the internal behavior of the SUT.
When we are writing a test in which we cannot (or chose not to) use a real depended-on component (DOC), we can replace it with a Test Double. The Test Double doesn't have to behave exactly like the real DOC; it merely has to provide the same API as the real one so that the SUT thinks it is the real one!