Czym jest Consumer-Driven Contract (CDC) i jakie problemy rozwiązuje
02.02.2023 | Ada Mazurkiewicz
Wstęp
Nikt nie ma wątpliwości, że aplikacje trzeba testować. Problemy pojawiają się gdy przechodzimy do pytania jak. Dziś skupimy się na backendzie. Wykorzystamy JAVA i odpowiemy na pytanie czym jest Consumer-Driven Contract (CDC).
Rodzaje testów
Najpierw powiedzmy sobie czym są i jakie problemy napotykamy wykorzystując różne rodzaje testów.
- Testy jednostkowe – ograniczają swój zasięg do jednej jednostki kodu, zazwyczaj metody lub obiektu w przypadku programowania obiektowego, procedury w programowaniu proceduralnym. Testy jednostkowe są względnie szybkie i wyizolowane, ale nie sprawdzają żadnych interakcji pomiędzy elementami.
- Testy integracyjne – sprawdzają powiązania pomiędzy elementami. Bazują na stubach i mockach, dlatego mogą nie odzwierciedlać rzeczywistego stanu odpowiedzi dostawcy.
- Testy end-to-end – weryfikują rzeczywistą komunikację z usługami zewnętrznymi (bazami danych, mikroserwisami itd.). Wymagają one, uruchomienia aplikacji (zarówno testowanej, jak i istnienia i uruchomienia usług zewnętrznych). Blokują środowisko, na którym testowany jest zestaw testów. Ponadto są bardzo czasochłonne i trudne w debugowaniu.
Consumer-Driven Contract
Uzupełnieniem powyższych rodzajów testów jest podejście Consumer-Driven Contract. Idea Consumer-Driven Contract polega na stworzeniu - przez stronę dostarczającą usługę (providera)- API w postaci dokumentu. Usługodawca udostępnia taki dokument odbiorcy. Każda ze stron uruchamia testy po swojej stronie. Dzięki temu klient pracuje z mockiem odzwierciedlającym stan faktyczny. Usługodawca testami kontraktowymi sprawdza aktualność wyspecyfikowanego i przekazanego dokumentu. Zerwanie kontraktu przez jedną ze stron, skutkuje przerwaniem procesu budowania jej aplikacji. Warto zaznaczyć, że testy te nie służą do symulowania pełnego zachowania funkcji biznesowych, a jedynie kontraktów pomiędzy aplikacjami.
Implementacja
Kiedy już znamy ideę i zalety CDC przejdźmy do rozwiązań technicznych. Najbardziej rozpowszechnionymi rozwiązaniami Javy do CDC są Spring Cloud Contract i Pact. W ramach przykładu zastosuję Spring Cloud Contract.
Usługodawca
Po stronie dostarczającej usługę dodajemy zależność:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-verifier</artifactId>
<scope>test</scope>
</dependency>
W tym samym pliku dodajemy plugin spring-cloud-contract-maven-plugin. Pozwoli to na zbudowania stuba. Plugin ten tworzy również testy sprawdzające, czy aplikacja jest spójna z kontraktem.
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>3.1.3</version>
<extensions>true</extensions>
<configuration>
<testFramework>JUNIT5</testFramework>
<baseClassForTests>com.example.producer.BaseContractTest</baseClassForTests>
</configuration>
</plugin>
Tag <extensions> określa, czy w procesie budowania mogą być wykorzystane inne artefakty. Jego wartość musi być ustawiona na true.
Pomiędzy znacznikami <configuration> umieszczamy klasę bazową (wygenerowane testy będą dziedziczyć po tej klasie) dla testów spójności z kontraktem. W przykładzie BaseContractTest. Następnie przygotowujemy kontrakt, który może być zapisany w pliku z rozszerzeniem groovy lub yaml. W zaprezentowanym przykładzie dodajemy plik Groovy i umieszczamy w nim zawartość kontraktu według poniższego schematu.
Contract.make {
description("should return book by id")
request {
url("/books/1")
method GET()
}
response {
status OK()
body(
title: "title",
id: 1
)
headers {
contentType('application/json')
}
}
}
Kolejnym krokiem jest napisanie wspomnianej wcześniej klasy bazowej.
@SpringBootTest
@AutoConfigureMockMvc
class BaseContractTest {
@Autowired
private MockMvc mockMvc;
@BeforeEach
public void setup() {
RestAssuredMockMvc.mockMvc(mockMvc);
}
}
Tworzymy kontroler:
@RestController
public class BookController {
@GetMapping(value = "/books/{id}")
public Book getBookById(@PathVariable Long id) {
return new Book("title", id);
}
}
Ostatnim krokiem jest wygenerowanie stuba i testów. Jeśli wykorzystujemy Maven, to wykonujemy polecenie mvn clean install. Testy pojawią się w folderze \target\generated-test-sources\contracts.
Konsument
W aplikacji konsumenta musimy dodać zależność pozwalającą uruchomić stub.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
<scope>test</scope>
</dependency>
Przy użyciu adnotacji @AutoConfigureStubRunner tworzymy testy z wykorzystaniem wygenerowanych stubów. Adnotacja ta przyjmuje parametr stubsMode, określający czy stub pobierany jest z lokalnego, czy zdalnego repozytorium, a także ids zawierający nazwy posiadanych stubów. Adnotacja StubRannerPort wstrzykuje adres portu, na którym uruchomiony jest stub. Test polega na wysłaniu żądania http do usługi producenta, którą reprezentuje utworzony przez niego stub.
@SpringBootTest
@AutoConfigureStubRunner(
stubsMode = StubRunnerProperties.StubsMode.LOCAL,
ids = "com.example:producer")
class ClientApplicationTests {
@StubRunnerPort("com.example:producer")
int port;
@Test
void contextLoads() {
var restTemplate = new RestTemplate();
var response = restTemplate.getForEntity("http://localhost:" + port + "/books/1", Book.class);
assertEquals(response.getBody().title(), "title");
}
}
Uruchamiamy testy i gotowe.
Podsumowanie
Consumer-Driven Contract służy do testowania integralności pomiędzy aplikacjami. W przeciwieństwie do testów integracyjnych zapewnia aktualność danych. CDC Nie wymaga uruchomienia usługi zewnętrznej, jak to jest w przypadku testów end-to-end. Testy jednostkowe oraz integracyjne testują poprawność w ramach jednej usługi. CDC nie zastępuje wyżej wymienionych rodzajów testów, ale je uzupełnia.