Podczas pisania testów z wykorzystaniem PHPUnit często pojawia się zagadnienie związane z przekazywaniem zmiennych pomiędzy poszczególnymi testami.
Załóżmy bowiem sytuację, gdy mamy test, w którym tworzymy sobie instancję jakiegoś obiektu i sprawdzamy działanie jednej z metod. W kolejnym teście chcemy przetestować kolejną z metod. Prześledźmy to może na przykładzie.
Testowana klasa:
class Car { public function __construct() {} public function startEngine() { return 'Engine started'; } public function turnLeft() { return 'Turning left'; } public function turnRight() { return 'Turning right'; } public function stopEngine() { return 'Engine stopped'; } }
Klasa testowa dla powyższej klasy:
class CarTest extends PHPUnit_Framework_TestCase { public function testShouldStartEngine() { $car = new Car(); $this->assertEquals('Engine started', $car->startEngine()); } public function testShouldStopEngine() { $car = new Car(); $this->assertEquals('Engine stopped', $car->stopEngine()); } }
Jak więc widzimy na powyższym przykładzie w każdym z testów tworzona jest nowa instancja obiektu Car. Dla powyższego przykładu nie jest to zbyt skomplikowane i można sobie zadać pytanie po co kombinować. Ale co w sytuacji, jeżeli nasz obiekt jest dość rozbudowany i chcielibyśmy w każdym kolejnym teście wywoływać na nim kolejną metodę i aby stan obiektu po wywołaniu metody był od razu dostępny w kolejnym teście, bez ponownego wywoływania wszystkich metod.
Rozwiązanie okazuje się bardzo proste. Wystarczy bowiem skorzystać z annotacji @depends, gdzie przeazujemy nazwę testu, który jest wykorzystywany do uruchomienia bieżącego testu. Dla powyższej klasy testowej wyglądałoby to następująco:
class CarTest extends PHPUnit_Framework_TestCase { public function testShouldStartEngine() { $car = new Car(); $this->assertEquals('Engine started', $car->startEngine()); return $car } /** * @depends testShouldStartEngine * @param Car car */ public function testShouldStopEngine($car) { $this->assertEquals('Engine stopped', $car->stopEngine()); } }
W teście testShouldStartEngine zwracamy instancję obiektu Car, która jest wykorzystywana w kolejnym teście, czyli testShouldStopEngine.
Rozwiązanie bardzo proste i skuteczne.
PHPUnit oferuje nam więcej ułatwień związanych z przygotowywaniem danych potrzebnych do wykonania poszczególnych testów tak, aby sama metoda testująca była maksymalnie prosta i przygotowywania obiektu nie zaciemniało jej ciała. Mamy między innymi dostępne metody: setUp, setUpBeforeClass, itp., ale o tym w kolejnym wpisie.
Ale czy takie podejście jakie jest sprzeczne z założeniem jakie powinno być stosowane w testach – każdy test jest nie zależy. Prezentowane rozwiązanie zmienia stan testowanego obiektu. Stosując podejście takie jak wyżej, test drugi w kolejności zależy od tego co się stanie w pierwszym.
Zaciemnia to wynik testów, teraz nie wiem czy problem był związany z metodą testowaną w pierwszym teście, czy jest to awaria całego obiektu (jakiś wewnętrznych mechanizmów)
Zgadza się, każdy test powinien być niezależny wówczas, gdy mówimy o testach jednostkowych. Z wykorzystaniem PHPUnit możemy tworzyć też smoke tests i sanity tests. Testujemy wówczas większy, aczkolwiek odpowiednio mały fragment aplikacji. Możemy np. przetestować poprawność wykonywania operacji na bazie danych – począwszy od stworzenia encji w bazie, poprzez jej modyfikację, wyszukanie, aż po usunięcie – słowem przetestowanie całego CRUD’a. Warto wówczas pracować na jednej konkretnej instancji obiektu, a nie tworzyć w każdym teście nową instację i nową encję.
Co do zaciemniania wyników testów – w codziennej pracy wykorzystuję te możliwości i nie stanowi dla mnie problemu znalezienie błędu w aplikacji. Gdy dany test nie przejdzie, automatycznie wszystkie testy od niego zależne nie są uruchamiane.
Ciężko mi się zgodzić z tezą, że zasady izolacji i niezależności testów nie aplikują się w jakimkolwiek z aspektów/technice testowania. W przypadku Twojego podejścia otrzymujemy “N” failujących testów nawet przy jednym błędzie w aplikacji przez co nie otrzymujemy natychmiastowej odpowiedzi co rzeczywiście nie działa. Pisząc “w codziennej pracy wykorzystuję te możliwości i nie stanowi dla mnie problemu znalezienie błędu w aplikacji. ” stwierdziłeś, że błędu musisz szukać, jednakże przy dobrzej siatce testów powinieneś wiedzieć co nie działa bez szukania 🙂
No nie do końca jest tak jak piszesz. Nie otrzymamy bowiem “N” failujących testów, bowiem fail będzie tylko na pierwszym teście, natomiast wszystkie od niego zależne się nie wykonają. Idąc dalej – test powinien testować jak najmniejszą część aplikacji, więc w przypadku faila wiem od razu, która część aplikacji nie zadziałała prawidłowo. Znalezienie błędu nie jest więc problemem – problemem jest słowo znalezienie. Nie oznacza ono jakiegokolwiek długotrwałego szukania. Znalezienie raczej to sprawdzenie, który test nie przeszedł i mamy już odpowiedź, gdzie powstał błąd.
W każdym bądź razie – pokazałem tu dostępną metodę, jednak samo jej wykorzystanie leży już w gestii programisty. Jeżeli mu będzie zaciemniać testy i ich wyniki może tego nie używać, jednak czasami może być to przydatne 😉
Pingback:PHPUnit – metody wywoływane przed i po testach | Mariusz Tulikowski – dev blog