Podwójnie parametryzowane testy w Spocku

detektywi.it 1 miesiąc temu

Jakiś czas temu wymieniałem implementację autoryzacji w systemie nad którym pracuje. Jednym z wymagań było zapewnienie poprzedniej funkcjonalności. Oczywiście mieliśmy testy na poprzednią funkcjonalność ale w jaki sposób użyć ich do testowania innej implementacji? W tym poście przedstawię tę historię.

Spock podobnie jak inne frameworki do testowania wspiera parametryzowane testy. zwykle definiuje się je przez tabelkę z wartościami zmiennych ale można też przekazać listę (data pipes), które potem zostają użyte w teście. O ile to rozwiązanie działa całkiem nieźle to da się je zastosować tylko do pojedynczej metody testowej. Nie możemy w ten sposób parametryzować całego “garnituru” testów tak aby przetestować różne implementacje tego samego interfejsu. Oczywiście, można do każdego testu w tabelce dodać kolejny testowany podmiot, jednak bardzo negatywnie wpłynie to na czytelność, zwłaszcza jeżeli mamy więcej niż jeden parametr i więcej niż jedną metodę testową.

Dla przykładu załóżmy iż mamy testy które testują interfejs List i ma on metodę empty więc przetestujemy ją.

import spock.lang.Specification class ArrayListTest extends Specification { List<String> subject; def setup() { subject = new ArrayList<String>() } def "new object should be empty"() { expect: subject.empty } }

Teraz chcielibyśmy dodać drugą implementację. Przy pomocy tabelki wyglądałoby to mniej więcej tak

import spock.lang.Specification class ArrayListTest extends Specification { def "#name new object should be empty"() { expect: subject.empty where: subject | name new ArrayList<String>() | "ArrayList<String>" Collections.emptyList() | "Collections.emptyList" } } ArrayListTest > #name new object should be empty > ArrayList<String> new object should be empty PASSED ArrayListTest > #name new object should be empty > Collections.emptyList new object should be empty PASSED

Aby to zrobić lepiej należy wydzielić testy do osobnej abstrakcyjnej klasy ListTest oraz zrobić dwie klasy które po niej będą dziedziczyć nadpisująć pola w metodzie setup, setupSpec, czy konstruktorze

import spock.lang.Specification abstract class ListTest extends Specification { abstract List<String> getSubject() def "new object should be empty"() { expect: subject.empty } } class ArrayListTest extends ListTest { List<String> subject = new ArrayList<String>() } class EmptyListTest extends ListTest { List<String> subject = Collections.emptyList() }

Metody testowe to zwykłe metody więc możemy swobodnie je dziedziczyć, a iż mamy dwie klasy które je uruchamiają testy dostaniemy podwójnie.

./gradlew test --tests '*ListTest' > Task :test ArrayListTest > new object should be empty PASSED EmptyListTest > new object should be empty PASSED

O ile tworzenie własnej klasy dziedziczącej po Specification nie jest niczym nowym i często widywałem to w testach które dzielą wspólny kontekst nie jest niczym nowym, to nie spotkałem się jeszcze z dziedziczeniem klas testowych. Warto zwrócić uwagę, iż klasa z testami musi być abstrakcyjna gdyż próba jej uruchomienia nie ma sensu bo pola są nie ustawione. Aby dodatkowo zabezpieczyć się przed niepoprawnym użyciem abstrakcyjnej klasy warto dodać metodę abstrakcyjną wywołaną w setup/Spec, tak aby już na etapie kompilacji sprawdzić czy test został poprawnie skonfigurowany.

Idź do oryginalnego materiału