Podwójnie parametryzowane testy w Spocku

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. 


Leave a Reply