Spring Cloud Contract i Pact są dojrzałymi już frameworkami do testów kontraktowych i mają zbliżoną funkcjonalność. Dużą różnicę można jednak zauważyć w podejściu do kontraktu na zwracanie listy wyników.
Załóżmy, że mamy endpoint zwracający wyniki wyszukiwania po dacie. Chcemy wołać go następująco:
/supplies?day=2018-12-23
W dobrym teście powinniśmy upewnić się, że parametr day jest w ogóle interpretowany, tzn. że serwer zmienia zwracane wyniki w zależności od tego, o jaką datę go poprosimy. Kontrakt piszemy więc taki: (given) serwer ma przypisane 2 elementy tylko dla 23 grudnia (when) pytamy go o 23 grudnia (then) zwraca dokładnie 2 elementy.
Spring Cloud Contract pozwala zapisać to następująco:
request { method 'GET' url('/supplies') { queryParameters { parameter day: '2018-12-23' } } } response { status OK() body([ [status: "CANCELED"], [count: 4, totalWeight: 20, status: "ACTIVE"], ]) headers { contentType('application/json') } }
Od razu widać pierwszy problem: mamy w kontrakcie części when i then, ale nie given. Kontrakt Pacta będzie zawierał wszystkie 3 części, specyfikacja umieszcza given jako stan providera.
Dalsze problemy pojawiają się przy wykonaniu. Załóżmy że mamy błąd w kodzie i nasz serwer duplikuje ostatni wynik. Zwracany JSON wygląda tak:
[ {"count": 151, "totalWeight": 980, "status": "CANCELED"}, {"count": 4, "totalWeight": 20, "status": "ACTIVE"}, {"count": 4, "totalWeight": 20, "status": "ACTIVE"} ]
Pact zgłasza błąd naruszenia kontraktu:
Expected a List with 2 elements but received 3 elements
Spring Cloud Contract twierdzi, że kontrakt jest spełniony. Jeśli zwrócimy za mało wyników, pojawi się błąd, ale jeśli za dużo — wszystko w porządku.
Dokumentacja Spring Cloud Contract wspomina o „eksperymentalnej obsłudze sprawdzania rozmiaru tablic”, natomiast u mnie opisywana tam opcja assertJsonSize
nie powoduje wygenerowania żadnych dodatkowych asercji na rozmiar tablic (może jest w bardzo wczesnej fazie eksperymentów).
Na upartego jesteśmy w stanie zmodyfikować kontrakt, by wyłapywać błędy w liczbie zwracanych wyników korzystając z matcherów:
body([ [status: "CANCELED"], [count: 4, totalWeight: 20, status: "ACTIVE"], ]) bodyMatchers { jsonPath('$', byType { minOccurrence(2) maxOccurrence(2) }) }
Dla mnie jest to jednak bardzo mało zadowalające. Tworzymy duplikację w samym teście: 3 razy zapisujemy, że wynik ma mieć 2 elementy — 2 razy w bodyMatchers
i raz określając samo body
. Musimy pamiętać, żeby przy zmianach w kontrakcie utrzymywać te 3 miejsca spójne ze sobą.
Patrząc ogólniej na zachowanie tego frameworku: choć DSL kontraktu w Spring Cloud Contract sugeruje, że w body
specyfikujemy wprost oczekiwaną odpowiedź, to prawda jest taka, że określamy jedynie luźno związane ze sobą stwierdzenia co do odpowiedzi. Możemy to lepiej zrozumieć patrząc, jak dopasowywane są pola.
Zapomnijmy o liczbie wyników i skupmy się na przypadku, gdy serwer źle przypisuje pola do pojedynczych wyników. Na przykład przestawia je jakby „na krzyż” (totalWeight 20 powędrowało z drugiego do pierwszego elementu):
[ {"count": 151, "totalWeight": 20, "status": "CANCELED"}, {"count": 4, "totalWeight": 980, "status": "ACTIVE"} ]
W przypadku Pacta nie ma zaskoczenia, framework zgłasza błąd na drugim z elementów:
$.1.totalWeight=[BodyMismatch(expected=20, actual=980, mismatch=Expected 20 but received 980, path=$.1.totalWeight, diff=null)]
Spring Cloud Contract akceptuje taką odpowiedź serwera. Dlaczego? Możemy łatwo znaleźć odpowiedź, ponieważ SCC generuje kod podczas buildu (w przeciwieństwie do Pacta, gdzie kontrakt podlega interpretacji w runtimie):
assertThatJson(parsedJson).array() .contains("['status']").isEqualTo("CANCELED"); assertThatJson(parsedJson).array() .contains("['count']").isEqualTo(4); assertThatJson(parsedJson).array() .contains("['totalWeight']").isEqualTo(20); assertThatJson(parsedJson).array() .contains("['status']").isEqualTo("ACTIVE");
Spring Cloud Contract nie operuje na poziomie elementów tablicy; dla tego frameworku tablica to „zupa” niepowiązanych ze sobą pól.
Da się wymusić poprawną weryfikację przez wpisanie wprost asercji do bodyMatchers
, ale ponownie: zabija to deklaratywność kontraktu i zmusza do kodowania w miejscu, gdzie nie mamy podpowiadania składni ani nawet kompilatora. Domyślne zachowanie SCC jest nieintuicyjne i w praktyce stanowi pułapkę zastawioną na nowych użytkowników.
Reakcję Pacta i SCC na łamanie kontraktu możecie samodzielnie zaobserwować uruchamiając kod z mojego repozytorium github.com/pkubowicz/contract-testing-samples.