Blog
Odwieczny dogmat TDD mówi, że test powinien zawierać tylko jedną asercję [1].
Zgadzam się, że jest to dobra reguła, jednak podobnie jak ze wszystkimi tego typu ekstremalnymi radami, nie powinieneś podążać za nią na ślepo. By lepiej zrozumieć, czemu zasada pojedynczej asercji jest przydatna, skupmy się na tym, jaki wpływ wywiera ona na dwa kluczowe aspekty testowania w “stylu” BDD: bycie żywą dokumentacją oraz sterowanie designem systemu.
Testy jako żywa dokumentacja
By móc służyć jako żywa dokumentacja, nasze testy muszą być przede wszystkim czytelne. Czy ilość asercji per test ma jakikolwiek wpływ na czytelność? By odpowiedzieć na to pytanie, zastanówmy się, co może być przyczyną umieszczenia w teście więcej niż jednej asercji. Istnieją dwie możliwości:
Pomieszanie dwóch zagadnień w jednym teście
Pomieszanie dwóch niezwiązanych ze sobą zagadnień, owocuje zazwyczaj testem takim jak ten:
describe('Anti-spam bot', function() {
it('should ban spammer and notify admin', function() {
antiSpamBot.checkOut(spammer);
expect(spammer).toBeBanned();
expect(admin).toBeNotified();
});
});
Mamy dwie akcje (“banowanie” i powiadamianie), dwa podmioty (spammer i admin) oraz operator logiczny “i” (“and”). Czyni to test niepotrzebnie skomplikowanym i mniej czytelnym (jak również utrudnia zlokalizowanie problemu, gdy test nie przechodzi, ale nie będę się tutaj skupiał na tym aspekcie).
Możemy spróbować zmniejszyć stopień komplikacji nazwy testu, zmieniając ją na bardziej ogólną, np. it('should handle spammer correctly', ...);
, ale tego typu nazwa jest zła z punktu widzenia dokumentacji, ponieważ ukrywa szczegóły tego, jakie akcje powinny się wydarzyć.
Zobaczmy jednak, co by się stało, gdybyśmy nie pozwolili na więcej niż jedną asercję. Zmusza nas to do przerobienia testu w taki sposób:
describe('Anti-spam bot', function() {
beforeEach(function() {
antiSpamBot.checkOut(spammer);
});
it('should ban spammer', function() {
expect(spammer).toBeBanned();
});
it('should notify admin', function() {
expect(admin).toBeNotified();
});
});
Powoduje to, że wszystkie zagadnienia są rozdzielone a nazwy testów czystsze i zwięzłe, co zwiększa czytelność i wartość naszych testów jako dokumentacji.
Rozpatrzmy teraz drugi przypadek, który może owocować więcej niż jedną asercją per test:
Pojedyncze zagadnienie z wieloma powiązanymi asercjami
Wyobraźmy sobie tego typu test:
describe('User', function() {
it('should have a non empty address', function() {
var address = user.address;
expect(address.streetName).not.toBeEmpty();
expect(address.streetNumber).not.toBeEmpty();
});
});
Pomimo dwóch asercji, powyższy test jest wolny od wszelkich problemów z poprzedniego przykładu, ponieważ nadal obejmuje on tylko jedno zagadnienie. Zawiera tylko jeden podmiot, nie zawiera łączników “i”, i jest bardzo zwarty i czytelny. Nie ukrywa też żadnego zachowania – nazwa User should have a non empty address
jest precyzyjną specyfikacją; to z jakich elementów składa się adres jest detalem implementacji, nieistotnym z punktu widzenia dokumentacji.
Jednakże, mimo że powyższy fragment jest przykładem czytelnego testu, jest on jednocześnie przykładem niezbyt dobrze zaprojektowanego kodu. Co prowadzi nas do drugiego ważnego aspektu testów w stylu BDD:
Testy sterujące designem systemu
W poprzednim przykładzie występuje brzydki “zapach”. Klasa User
jest zaśmiecona szeregiem pól związanych z adresem. Czemu należą one bezpośrednio do klasy User
? (na marginesie: jeśli przyjrzysz się uważnie testowi, możesz zauważyć kolejny, subtelniejszy “zapaszek” – złamanie Prawa Demeter).
Przyjrzyjmy się znów, co by się stało gdybyśmy posłuchali zasady jednej asercji per test. Jak możemy zweryfikować, że użytkownik ma niepusty adres, jeśli nie wolno nam sprawdzać więcej niż jednego pola adresu na raz?
Bardzo prosto – możemy potraktować to, co opisuje nazwa naszego testu (User should have a non empty address
– czy jeszcze prościej: User should have an address
) dosłownie:
describe('User', function() {
it('should have an address', function() {
expect(user.address).not.toBeNull();
});
});
describe('Address', function() {
it('should have non empty street name', function() {
expect(address.streetName).not.toBeEmpty();
});
it('should have non empty street number', function() {
expect(address.streetNumber).not.toBeEmpty();
});
});
Obecny design jest o wiele lepszy. User
ma teraz pojedyncze pole Address
(które może albo istnieć, albo nie, przez co weryfikacja staje się dużo prostsza). Address
staje się niezależną klasą, która nadaje się do wykorzystania w innych miejscach, może zawierać swoje własne reguły walidacji i jest znacznie łatwiejsza do bezpośredniego przetestowania niż pośrednio, poprzez klasę User
. (I przy okazji rozwiązaliśmy problem Prawa Demeter).
Wszystkie te korzyści pojawiły się, ponieważ “słuchaliśmy naszych testów” – w szczególności tego, że zawierały więcej niż jedną asercję.
Czy ZAWSZE musi być tylko jedna asercja?
Oczywiście, że nie. Jak z każdą zasadą, nie jest to uniwersalne prawo, i ma swoje ograniczenia. Istnieją uzasadnione przypadki, w których testy są bardziej czytelne jeśli zawierają więcej niż jedną asercję (więcej na ten temat w jednym z kolejnych postów).
Jednakże, więcej niż jedna asercja per test to “zapach”, którego nie powinieneś ignorować (szczególnie jeśli towarzyszą mu złożone nazwy testów, zawierające łaczniki “i”). Zazwyczaj, taki “zapach” próbuje Ci powiedzieć, że pomieszałeś dwa niezależne zagadnienia w jednym teście, albo że w Twoim kodzie czai się ukryta klasa, czekająca na ujawnienie.
_[1] lub specyfikacja jedno oczekiwanie, jeżeli używamy nomenklatury BDD; po polsku jednak brzmi to dość karkołomnie, będę więc trzymał się nomenklatury “tradycyjnego” TDD, mimo że we wszystkich przykładach używam składni BDD_
Jak to wygląda w Twoim przypadku? Próbujesz stosować się do zasady “jednej asercji per test”? Jak ściśle? Podziel się swoimi doświadczeniami w komentarzach poniżej!