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. Zazwyczaj 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śli mamy więcej niż jeden parametr i więcej niż jedną metodę testową.
Dla przykładu załóżmy że 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 że 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ę, że 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.