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.