Spock jest frameworkiem do testów, którego popularność w świecie JVM-a jest nie do podważenia.
W internecie można znaleźć wiele tutoriali, które wprowadzają w podstawy adnotacji @Unroll
. Ogólnie chodzi w niej o to, że mamy możliwość uruchomienia tego samego kodu z wieloma zestawami danych. Najprostszy przykład użycia @Unroll
wygląda tak:
1 2 3 4 5 6 7 8 9 10 |
def "maximum of two numbers"(int a, int b, int c) { expect: Math.max(a, b) == c where: a | b | c 1 | 3 | 3 7 | 4 | 4 0 | 0 | 0 } |
Interesowało mnie, ile jeszcze można wycisnąć z tej adnotacji, i o tym będzie ten post.
Wymagania
Przy pisaniu procesorów, które tworzą encje takie, jakie Localization, potrzebowałem przetestować sytuację, gdy do procesora wchodzi encja reprezentująca stronę z Memory Alpha, posiadająca jakiś zestaw kategorii. Na podstawie obecności tych kategorii były ustawiane konkretne booleanowe flagi w encji wyjściowej, w tym przypadku właśnie Localization. Gdybym chciał to zapisać w kilkunastu czy kilkudziesięciu testach, powtórzyłbym bardzo dużo kodu.
Jednocześnie chciałem sprawdzić w każdej iteracji więcej, niż jedną daną wyjściową, ponieważ obok flagi, która w sytuacji obecności danej kategorii miała być ustawiona na prawdę, chciałem też sprawdzać liczbę nienullowych pól w całej encji wyjściowej.
Trzecim wymaganiem było, by nazwa flagi była przechowywana jako string. To także okazało się możliwe z @Unrollem
. Chociaż zapewne to głównie zasługa Grooviego, który pozwala na dostęp do obiektów z użyciem tej samej składni, która umożliwia dostęp do map.
Ostatnim wymaganiem było, by zapisać w metodzie testowej interakcje, najlepiej z wyspecyfikowaną ich ilością. To też się prawie udało.
Implementacja
Ostateczna implementacja wygląda tam:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
class LocationPageProcessorTest extends Specification { private LocationPageFilter locationPageFilterMock private PageBindingService pageBindingServiceMock private GuidGenerator guidGeneratorMock private CategoryTitlesExtractingProcessor categoryTitlesExtractingProcessorMock private LocationNameFixedValueProvider locationNameFixedValueProviderMock private LocationPageProcessor locationPageProcessor void setup() { locationPageFilterMock = Mock() pageBindingServiceMock = Mock() guidGeneratorMock = Mock() categoryTitlesExtractingProcessorMock = Mock() locationNameFixedValueProviderMock = Mock() locationPageProcessor = new LocationPageProcessor(locationPageFilterMock, pageBindingServiceMock, guidGeneratorMock, categoryTitlesExtractingProcessorMock, locationNameFixedValueProviderMock) } @Unroll('set #flagName flag when #page is passed; expect #trueBooleans not null fields') void "sets flagName when page is passed"() { given: categoryTitlesExtractingProcessorMock.process(_ as List<CategoryHeader>) >> { List<CategoryHeader> categoryHeaderList -> Lists.newArrayList(categoryHeaderList[0].title) } locationNameFixedValueProviderMock.getSearchedValue(_) >> FixedValueHolder.empty() expect: Location location = locationPageProcessor.process(page) location[flagName] == flag ReflectionTestUtils.getNumberOfTrueBooleanFields(location) == trueBooleans where: page | flagName | flag | trueBooleans new SourcesPage(categories: Lists.newArrayList()) | 'establishment' | false | 0 new SourcesPage(categories: createList(CategoryTitle.EARTH_LOCATIONS)) | 'earthlyLocation' | true | 1 new SourcesPage(categories: createList(CategoryTitle.EARTH_LANDMARKS)) | 'earthlyLocation' | true | 3 new SourcesPage(categories: createList(CategoryTitle.EARTH_LANDMARKS)) | 'structure' | true | 3 new SourcesPage(categories: createList(CategoryTitle.EARTH_LANDMARKS)) | 'landmark' | true | 3 // Tutaj było jeszcze więcej danych, ale darujmy je sobie } private static List<CategoryHeader> createList(String title) { Lists.newArrayList(EtlTestUtils.createCategoryHeaderList(title)) } } |
Opis implemenacji
W bloku given
zostały zapisane wszystkie wymagane w każdej iteracji interakcje. Można by też po prostu dostarczyć do metody testowej już zaprogramowane stuby, które miałyby te same interakcje.
W bloku expect
pobieramy lokalizację z procesora, a następnie, z użyciem nazwy flagi, którą też mamy w datasecie, porównujemy wartość flagi z kolejną wartością zapisaną w datasecie. Ostatecznie refleksją sprawdzamy ilość nienullowych pól w encji reprezentującej lokalizację.
W bloku where
znajdują się dane. Tutaj największą niespodzianką jest to, że można w ramach bloków konstruować obiekty i odwoływać się o innych metod. Patrząc na przykłady w sieci, nie jest to wcale oczywiste.
Ograniczenia
Nie znalazłem sposobu, by wyspecyfikować ilość interakcji w ramach jednej metody testowej z @Unrollem
. Interakcji nie można umieszczać w blokach given
i expect
, a z kolei blok then
nie może współegzystować z blokiem expect
. Jeśli więc chcemy sobie zaprogramować interakcje, możemy to zrobić tylko w bloku given
, ale bez zapisywania przy nich ilości.
Podsumowanie
Umiejętnie użyty @Unroll
pozwala napisać dowolny test, z wyjątkiem takiego, który zlicza ilość interakcji.
Kiedy używać @Unrolla
, a kiedy jest on overkillem? Myślę, że dobrą granicą są cztery bardzo podobne metody testowe. Jeśli tyle mamy, należałoby je połączyć w jedną metodą z @Unrollem
. Ale zgodnie z duchem reguły DRY, nawet 2 podobne metody testowe to nie byłoby za dużo, by zacząć używać @Unrolla
.
A co, jeśli mimo wszystko chcemy zliczać interakcje i mieć data tables? Moje doświadczenie pokazuje, że lepiej rozdzielać kod, który głównie operuje na interakcjach z zależnościami, od kodu, który głównie operuje na danych. Jeśli powstaną dwie osobne klasy, albo przynajmniej dwie osobne publiczne metody, z których jedna operuje głównie na zależnościach klasy, a druga głównie przetwarza dane, problem z brakiem zliczania interakcji przy użyciu @Unrolla
powinien zostać zminimalizowany.