Blog

06 marca 2015 Wojciech Zawistowski

Tylko jedna asercja per test – pusty dogmat czy nie?

Tagi:

Tylko jedna asercja per test – pusty dogmat czy nie?

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!

Zobacz na blogu

09.09.2022
Marcin Jahn
It’s Not Just HTTP It’s Not Just HTTP

In today’s world of cloud-based solutions, distributed systems, and microservices-based architectures, network communication is a...

23.08.2022
Adam Mrowiec
Konferencja IPC 2022 Berlin Konferencja IPC 2022 Berlin

Pandemia wreszcie się kończy, dlatego w tym roku postanowiliśmy wrócić do naszych wyjazdów na konferencje....