poniedziałek, 5 sierpnia 2013

... a test, żeby testował jedną rzecz...

to zależy

Ostatnio mieliśmy w pracy prezentacją dotyczącą test double (swoją drogą polecam zainteresować się tematem i zacząć stosować wszelkiego rodzaju mocki itp., programista nawet nie zdaje sobie sprawy, jak wpływają one na jakość tworzonego kodu i czytelność testów).
Ale ja nie o tym (przynajmniej nie dzisiaj :).
Po prezentacji (i w jej trakcie) poruszyliśmy kilka tematów dotyczących testowania w ogóle i jednym z owych tematów było pytanie: czy jeden test powinien składać się z jedej asercji?
Jak to zazwyczaj bywa z takimi ideologicznymi pytaniami ludzie się podzielili na dwie grupy i zaczęliśmy dyskutować i wymieniać się poglądami. Nie muszę dodawać, że konkluzją było stwierdzenie "to zależy" :)

Tylko czy rzeczywiście "zależy"? A jeżeli tak, to od czego?

złe pytanie kością niezgody

Przysłuchując się całej rozmowie i opierając się na późniejszych przemyśleniach śmiem twierdzić, że powodem niezgody pomiędzy nami, było źle sformułowane pytanie. Zwróćcie uwagę, że padło tam słowo "asercja" i sądzę, że części ludzi (w tym ja) identyfikowała jedną asercję z testowaniem jednej rzeczy, a pozostali widzieli to jako jedną z metod z tymże słowem w nazwie. Różnice w interpretacji pytania były powodem różnic w stanowiskach.

A jak powinno brzmieć pytanie?
Czy jeden test powinien testować jedną rzecz?

Zdaję sobie sprawę, że dyskusji na ten temat było wiele, ale mimo to pokuszę się o dorzucenie swoich kilku groszy.

dowód nie wprost

Zacznijmy od testu, który sprawdza wiele rzeczy:
/**
 * @test
 */
public function personCreatedWithAllSettings()
{
  $person = new Person("Sebastian", "Malaca", new Date());
  $person->addAddress(new Address());
  $person->addSkill(new Skill("OOP"));
  $person->addInterest(new Interest("Comic books"));
  $person->inRelationshipWith(new Person());
  /** itp. itd. **/ 

  assertEquals($expectedDescription, $person->description);
  assertTrue($person->inRelationship());
  assertTrue($person->gotPlaceToLive());
  assertEquals($expectedAddress, $person->whereLives());
  assertFalse($person->isInterestedIn(new Interest("hockey"));
  /** itp. itd. **/ 
}

Mieliście do czynienia z takimi testami? Ja niestety tak i szczerze się przyznam, że pewnie niejeden taki sam stworzyłem.
Oczywiście idea, która stała za napisaniem takiego testu była z pewnością szczytna (jak większość ideii :), jednak pomimo chęci, test taki nie jest wcale tak pomocny jakby mógł być. Dlaczego?

co mi chcesz powiedzieć?

O czym powyższy test nas informuje? O tym, że kilka metod działa tak jak powinno? Jednak ile czasu zajmie nam powiązanie tych metod ze sobą? I co da nam pewność, że pozornie niezależne metody w jakiś sposób na siebie nie odziałowują? Co da nam pewność, że nie jest to jakiś "specjalny przypadek" i wszystkie warunki, które tutaj zostały spełnione sprawiają, że asercje zachowują się w ten, a nie w inny sposób? Czy nazwa testu nam coś mówi?
Na powyższe pytania odpowiedzcie sobie sami i zastanówcie się, czy posiadanie tylko jednej metody jest wystarczająco przekonywującym argumentem, żeby stracić na czytelności?

Czyż nie lepiej byłoby posiadać testy takie jak:
/**
 * @test
 */
public function hasNoPlaceToLive()
{
  $person = new Person();
  
  assertFalse($person->gotPlaceToLive());
}

/**
 * @test
 */
public function hasPlaceToLiveWhenAddressIsSet()
{
  $person = new Person();
  $person->addAddress(new Address());

  assertTrue($person->gotPlaceToLive());
}

Czy takiego testu nie czyta się przyjemniej? Czy nie daje nam on dokładniejszych informacji? Teraz nie trzeba się zastanawiać, czy któreś metody na siebie nie odziałowują, bo mamy pewność, że tak nie jest.

testy i dokumentacja

Odpowiednio małe testy, które sprawdzają dokładnie jeden przypadek, testują jedną rzecz, potrafią z powodzeniem zastąpić dokumentację.
Wyobraźcie sobie, że chcecie wysłać obiekt klasy Person do pracy. Szukacie w API metody startNewJob() i okazuje się, że wylatuje Wam wyjątek, który informuje o tym, że dany obiekt ma za mało lat. I co? obiektu ruszyć nie możecie, więc może trzeba będzie modyfikować klasę? Albo poszperać trochę, bo a nóż ktoś miał podobny problem do rozwiązania?
Szukacie i szukacie aż w końcu udaje Wam się dowiedzieć, że taki obiekt może iść do pracy pod warunkiem, że dostanie pozwolenie od swoich rodziców :) Ciężko było się domyślić?
Czy nie lepiej byłoby mieć dobry zestaw testów, przez który można było przejść, przeczytać nazwy metod i znaleźć jeden sendKidToWork() ?

Może przykład jest trywialny i uśmiechnęliście się pod nosem, ale są klasy (moduły, paczki), gdzie takie testy naprawdę ułatwiają życie. Nawet jeżeli są diagramy pokazujące zachowanie (a pisze to osoba lubująca się w UML'u :).

jedna asercja to nie jeden test

Wyobraźcie sobie, że testujecie klasę budującą dokument xls. Co należy sprawdzić, żeby mieć pewność, że działa tak jak należy? W podstawowej wersji, co najmniej to, czy ma odpowiedni nagłówek oraz czy jest pustą stroną. To już dwie asercje, a test jest naprawdę prosty. A co w przypadku, gdy będziecie musieli sprawdzić, czy po ustawieniu wielu rzeczy dokument nadal wygląda tak, jak powinien? Przecież tam będzie ich jeszcze więcej?

Testowanie jednej rzeczy nie oznacza, że można wywołać tylko jedną metodę z obiektu, a sprawdzić wszystko należy jedną asercją. Chyba zgodzicie się, że w takim wypadku nie przetestowalibyśmy wielu istotnych rzeczy :) Chodzi o to, żeby zarówno wywoływane na obiekcie metody, jak i asercje dotyczyły jednej rzeczy, jednego przypadku. Wtedy nawet 20 i więcej metod sprawdzających to nadal jedna i ta sama asercja.

kiedy warto podzielić test?

Poniższe punkty to nie są prawdy objawione, których należy się bezwzględnie trzymać, ale pokazują, kiedy powinniśmy mieć się na baczności i zastanowić co najmniej jeszcze raz :)
  • asercje jako zdanie złożone
    Jeżeli pomiędzy asercjami nie zachodzi koniunkcja tylko alternatywa, czyli jesteście w stanie wydzielić jakieś dwa podzbiory i niespełnienie jednej asercji nie jest jednoznaczne z tym, że wszystko działa źle, to jest to znak, że test można podzielić.
  • nie umiecie nazwać testu
    Jeżeli macie problem z nazwaniem testu to często pokazuje nam to, że testowanych jest zbyt wiele rzeczy jednocześnie.
  • łączniki w nazwie
    Jeżeli w nazwie testu macie niepohamowaną potrzebę użycia zdania złożonego (jeżeli występuje np. 'i' lub 'lub') to również jest to wskazówka, że test może dotykać więcej niż jednej rzeczy.

przetestujcie to sami

Ludzie są zazwyczaj sceptycznie nastawieni do wszelkich innych podejść, ale radzę Wam spróbować. Z pewnością nie stracicie, a może sami przekonacie się, że taki dzielenie testów ma sens i przynosi korzyści.

A jeżeli już praktykujecie takie pisanie testów, to podzielcie się w komentarzach Waszymi sposobami na wyłapywanie tych zbyt dużych.