Warstwa Abstrakcji - stanowi izolację pomiędzy kodem programisty a zewnętrznymi modułami, pozwala odseparować system od jego zależności przez co czyni go podatnym na zmiany i aktualizacje. Przywiązując system do innej usługi czynimy go słabym.

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!


Masz pytanie?
Napisz: [email protected]
Akceptuję

Ten serwis używa plików cookies. Więcej o plikach cookies.