Spring Cloud Contract Sinhala Tutorial | Consumer-Driven Contracts Explained

Spring Cloud Contract Sinhala Tutorial | Consumer-Driven Contracts Explained

ආයුබෝවන් Developer යාළුවනේ!

අද අපි කතා කරන්න යන්නේ Microservices ලෝකයේදී අනිවාර්යයෙන්ම දැනගෙන ඉන්න ඕන කරන හරිම වැදගත් සංකල්පයක් ගැන – ඒ තමයි Consumer-Driven Contracts (CDC). විශේෂයෙන්ම, අපි මේක Spring Boot Application එකකදී Spring Cloud Contract Framework එක පාවිච්චි කරලා කොහොමද implement කරන්නේ කියලා පියවරෙන් පියවර බලමු.

Microservices කියන්නේ නියමයි නේද? Faster development, scalability, independent deployments... හැබැයි, මේවා එකිනෙකට කතාබහ කරනකොට එන ප්‍රශ්න තියෙනවා. Services අතර communication එකේදී පොඩි වෙනසක් ආවොත්, whole system එකම කඩාගෙන වැටෙන්න පුළුවන්. ඒ වගේ වෙලාවට තමයි Integration Testing කියන එක headache එකක් වෙන්නේ.

මේ tutorial එක අවසාන වෙනකොට ඔයාට මේ දේවල් ගැන පැහැදිලි අවබෝධයක් ලැබෙයි:

  • Consumer-Driven Contracts (CDC) කියන්නේ මොකක්ද සහ ඇයි ඒවා වැදගත් වෙන්නේ කියලා.
  • Microservices Integration Testing වලදී CDC වලින් විසඳන ප්‍රශ්න මොනවද කියලා.
  • Spring Cloud Contract Framework එක CDC implement කරන්න අපිට උදව් කරන්නේ කොහොමද කියලා.
  • Practical Example එකක් හරහා Contract එකක් define කරන හැටි සහ verify කරන හැටි.

එහෙනම්, අපි පටන් ගමු!

Consumer-Driven Contracts (CDC) කියන්නේ මොකක්ද? (ඇයි මේ CDC වැදගත්?)

හිතන්න, ඔයාගේ System එකේ Service A කියලා එකක් තියෙනවා, ඒකෙන් Service B එකේ API එකක් call කරනවා කියලා. සාමාන්‍යයෙන් වෙන්නේ මොකක්ද? Service B හදන Developer කියනවා 'මගේ API එක මේ වගේ response එකක් දෙනවා' කියලා. Service A හදන Developer ඒක බලාගෙන client එක හදනවා.

ඒත්, Service B Developer ට අමතක වුණොත්, නැත්නම් පොඩි වෙනසක් කළොත්, Service A එක කැඩෙනවා. මේක හරිම අවුල් සහගත තත්ත්වයක්. මේ වගේ වෙලාවට තමයි අපි Integration Tests ලියන්නේ. ඒත්, Microservices ගොඩක් තියෙනකොට මේ Integration Tests ලියන එකයි, maintain කරන එකයි හරිම අමාරු වැඩක්.

මෙන්න මේකට තමයි Consumer-Driven Contracts (CDC) කියන concept එක එන්නේ. සරලවම කිව්වොත්, CDC කියන්නේ:

"API එකක් පාවිච්චි කරන Consumer (භාවිතා කරන්නා) ට අවශ්‍ය දේ (Input/Output) Contract එකක් විදියට define කරනවා. ඊට පස්සේ, API එක සපයන Producer (සපයන්නා) මේ Contract එකට අනුව තමන්ගේ API එක හදලා, ඒක ඒ Contract එක satisfy කරනවද කියලා verify කරනවා."

මේක හරියට, ඔයාට බඩු ටිකක් ඕන කරනවා කියලා ඔයාගේ Supplier ට ලියපු Note එකක් වගේ. Supplier ඒ Note එක බලලා බඩු ටික දෙනවා. Supplier ට බඩු ටික හරියටම දුන්නද කියලා තහවුරු කරන්නත් ඒ Note එකම පාවිච්චි කරන්න පුළුවන්.

CDC වලින් විසඳන ගැටලු:

  1. Integration Test Hell අඩු කරනවා: Services අතරේ Integration Tests ගොඩක් ලියන එක අඩු කරනවා. ඒ වෙනුවට, Producer තමන්ගේ API එක Contract එකට අනුව වැඩ කරනවද කියලා Unit Test එකක් වගේ run කරනවා. Consumer ට Producer ගේ Test environment එක ඕන වෙන්නේ නැහැ, stubs (ව්‍යාජ API) පාවිච්චි කරන්න පුළුවන්.
  2. Faster Feedback: Producer ට API එකේ වෙනසක් වුණාම, ඒක Consumer ට impact කරනවද කියලා ඉක්මනින් දැනගන්න පුළුවන්. Test එක fail වෙනවා, ඉතින් ඒක deploy කරන්න කලින්ම හදාගන්න පුළුවන්.
  3. Clear API Definition: Contract එක කියන්නේ Producer සහ Consumer දෙගොල්ලන්ටම තේරෙන පොදු භාෂාවක්. API එක කොහොමද වැඩ කරන්න ඕන කියලා පැහැදිලිව පෙන්වනවා.
  4. Fewer Surprises: Producer තමන්ගේ API එක වෙනස් කරනකොට, Consumer ට ඒක දැනගන්න කලින්ම, Contract Test එක fail වෙන නිසා වෙනස්කම් ගැන දැනුවත් වෙන්න පුළුවන්.

Spring Cloud Contract Framework එක (Spring Cloud Contract වලින් CDC කරමු)

Spring Cloud Contract කියන්නේ Spring ecosystem එකේ Consumer-Driven Contracts implement කරන්න තියෙන සුපිරිම Framework එකක්. මේක අපිට Groovy DSL (Domain Specific Language) එකක් පාවිච්චි කරලා Contracts ලියන්න පුළුවන්කම දෙනවා. මේකෙන් වෙන්නේ මොකක්ද කියලා බලමු:

  • Producer පැත්තේ: ඔයා ලියන Groovy Contract File එකෙන් Producer ගේ API එකට අදාළව Automated Tests Generate කරනවා. මේ Tests මඟින් Producer ගේ API එක Contract එකට අනුව වැඩ කරනවද කියලා verify කරනවා.
  • Consumer පැත්තේ: Producer ගේ Artifact එක publish කරනකොට, ඒ Contract එකට අදාළ Stubs (ව්‍යාජ API implementations) generate කරනවා. Consumer ට මේ Stubs තමන්ගේ Tests වලදී පාවිච්චි කරලා, Producer ගේ සැබෑ API එකට සම්බන්ධ වෙන්නේ නැතුවම තමන්ගේ client code එක test කරන්න පුළුවන්.

මේ නිසා, Producer සහ Consumer දෙන්නටම එකම Contract එකට අනුව වැඩ කරන්න පුළුවන්. මේකෙන් Microservices integration වලදී ඇතිවෙන අවුල් සහගත තත්ත්වයන් ගොඩක් අඩු වෙනවා.

Practical Example: Contract එකක් නිර්වචනය කරමු

අපි මේක පොඩි Example එකක් අරගෙන කරමු. හිතන්න, අපිට Book Service (Producer) එකක් තියෙනවා, ඒකෙන් පොත් විස්තර දෙනවා. තව Order Service (Consumer) එකක් තියෙනවා, ඒකෙන් Order එකක් හදනකොට Book Service එකෙන් පොත් විස්තර අරගන්නවා.

1. Project Setup (ප්‍රොජෙක්ට් එක හදාගමු)

අපි Gradle Build Tool එක පාවිච්චි කරමු. Project දෙකක් හදමු: book-service (producer) සහ order-service (consumer).

book-service (Producer) - build.gradle

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.5'
    id 'io.spring.dependency-management' version '1.1.4'
    id 'org.springframework.cloud.contract' version '4.1.2'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '17'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.cloud:spring-cloud-starter-contract-verifier'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

contracts {
    testMode = 'WEBCLIENT' // Or REST_ASSURED, MOCKMVC
}

tasks.named('test') {
    useJUnitPlatform()
}

වැදගත් දේ තමයි id 'org.springframework.cloud.contract' plugin එකයි, testImplementation 'org.springframework.cloud:spring-cloud-starter-contract-verifier' dependency එකයි. contracts block එකේ testMode එක set කරන්නත් අමතක කරන්න එපා.

order-service (Consumer) - build.gradle

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.5'
    id 'io.spring.dependency-management' version '1.1.4'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '17'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-webflux' // For WebClient
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.cloud:spring-cloud-starter-contract-stub-runner'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
    useJUnitPlatform()
}

මෙතනදී testImplementation 'org.springframework.cloud:spring-cloud-starter-contract-stub-runner' dependency එක වැදගත්.

2. Producer පැත්ත (book-service)

අපි සරල Book model එකක් සහ BookController එකක් හදමු.

Book.java

package com.example.bookservice.model;

public class Book {
    private Long id;
    private String title;
    private String author;
    private double price;

    public Book(Long id, String title, String author, double price) {
        this.id = id;
        this.title = title;
        this.author = author;
        this.price = price;
    }

    // Getters and Setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }
    public String getAuthor() { return author; }
    public void setAuthor(String author) { this.author = author; }
    public double getPrice() { return price; }
    public void setPrice(double price) { this.price = price; }
}

BookController.java

package com.example.bookservice.controller;

import com.example.bookservice.model.Book;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/books")
public class BookController {

    @GetMapping("/{id}")
    public ResponseEntity<Book> getBookById(@PathVariable Long id) {
        // In a real application, you would fetch this from a database
        if (id == 1L) {
            return ResponseEntity.ok(new Book(1L, "The Hitchhiker's Guide to the Galaxy", "Douglas Adams", 10.99));
        } else if (id == 2L) {
            return ResponseEntity.ok(new Book(2L, "1984", "George Orwell", 8.50));
        }
        return ResponseEntity.notFound().build();
    }
}

Contract Definition (Producer Side)

දැන් අපි contract එක ලියමු. මේක book-service/src/test/resources/contracts/book/shouldReturnBookDetails.groovy කියන path එකේ තියෙන්න ඕනේ.

package contracts.book

import org.springframework.cloud.contract.spec.Contract

Contract.make {
    description "Should return book details for a given ID"
    request {
        method 'GET'
        url '/books/1'
        headers {
            contentType(applicationJson())
        }
    }
    response {
        status 200
        headers {
            contentType(applicationJson())
        }
        body(
            id: 1,
            title: "The Hitchhiker's Guide to the Galaxy",
            author: "Douglas Adams",
            price: 10.99
        )
        // You can also use regular expressions for values if needed
        // body(id: $(client(1), server(regex('\d+'))), ...)
    }
}

මේ .groovy file එක තමයි අපේ Contract එක. මේකෙන් අපි කියනවා: "/books/1 කියන URL එකට GET request එකක් ආවොත්, 200 OK status එකක් එක්ක, මේ JSON body එක Response එක විදියට දෙන්න ඕනේ."

දැන් ඔයා book-service එකේ ./gradlew build command එක run කළොත්, Spring Cloud Contract Verifier plugin එකෙන් මේ Groovy Contract එක කියවලා, Producer API එක verify කරන්න Test Class එකක් generate කරනවා. මේ Test එක run වෙන්නේ ඔයාගේ Controller එකට එරෙහිවයි. ඒ Test එක pass වුණොත්, Producer API එක Contract එකට අනුව වැඩ කරන බව තහවුරු වෙනවා.

සාමාන්‍යයෙන්, මේ Test එක book-service/build/generated-snippets/org/springframework/cloud/contract/stubrunner/producer/BookServiceControllerTest.java වගේ තැනක generate වෙයි (path එක පොඩ්ඩක් වෙනස් වෙන්න පුළුවන්).

ඒ වගේම, මේ build process එකේදී, book-service/build/libs folder එක ඇතුළට book-service-0.0.1-SNAPSHOT-stubs.jar වගේ jar එකක් generate වෙනවා. මේක තමයි Consumer ට පාවිච්චි කරන්න පුළුවන් stubs තියෙන artifact එක.

3. Consumer පැත්ත (order-service)

දැන් අපි consumer service එක හදමු. මේක Book Service එකේ API එක call කරන client එකක්.

BookClient.java (Consumer Service එකේ)

package com.example.orderservice.client;

import com.example.orderservice.model.Book;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

@Component
public class BookClient {

    private final WebClient webClient;

    // In a real app, base URL would be configured (e.g., application.properties)
    public BookClient(WebClient.Builder webClientBuilder) {
        this.webClient = webClientBuilder.baseUrl("http://localhost:8090").build(); // StubRunner will run on this port by default
    }

    public Mono<Book> getBook(Long bookId) {
        return webClient.get()
                .uri("/books/{id}", bookId)
                .retrieve()
                .bodyToMono(Book.class);
    }
}

Book model එක Producer ගේ වගේම Consumer ගේ පැත්තෙත් තිබීම අවශ්‍යයි (හෝ පොදු library එකක් පාවිච්චි කිරීම). order-service/src/main/java/com/example/orderservice/model/Book.java

package com.example.orderservice.model;

public class Book {
    private Long id;
    private String title;
    private String author;
    private double price;

    // NoArgsConstructor, AllArgsConstructor, Getters, Setters (Lombok can simplify this)

    public Book() {}

    public Book(Long id, String title, String author, double price) {
        this.id = id;
        this.title = title;
        this.author = author;
        this.price = price;
    }

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }
    public String getAuthor() { return author; }
    public void setAuthor(String author) { this.author = author; }
    public double getPrice() { return price; }
    public void setPrice(double price) { this.price = price; }

    @Override
    public String toString() {
        return "Book{" +
               "id=" + id +
               ", title='" + title + '\'' +
               ", author='" + author + '\'' +
               ", price=" + price +
               '}';
    }
}

Consumer Test (order-service)

දැන් අපි Consumer Test එක ලියමු. මේකෙන් අපි BookClient එක test කරනවා, නමුත් සැබෑ Book Service එකට සම්බන්ධ වෙන්නේ නැහැ. ඒ වෙනුවට, Stub Runner එකෙන් generate කරන Stubs පාවිච්චි කරනවා.

order-service/src/test/java/com/example/orderservice/client/BookClientTest.java

package com.example.orderservice.client;

import com.example.orderservice.model.Book;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner;
import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

@SpringBootTest
@AutoConfigureStubRunner(
    ids = {"com.example:book-service:+:stubs:8090"}, // GroupId:ArtifactId:Version:Classifier:Port
    stubsMode = StubRunnerProperties.StubsMode.LOCAL // Use LOCAL if stubs JAR is in local Maven repo
)
public class BookClientTest {

    @Autowired
    private BookClient bookClient;

    @Test
    void shouldFetchBookDetailsFromStub() {
        // This call will hit the stub server provided by StubRunner
        // instead of the actual book-service.
        Book book = bookClient.getBook(1L).block(); // .block() for simplicity in test, use async in real app

        assertNotNull(book);
        assertEquals(1L, book.getId());
        assertEquals("The Hitchhiker's Guide to the Galaxy", book.getTitle());
        assertEquals("Douglas Adams", book.getAuthor());
        assertEquals(10.99, book.getPrice());
    }
}

මේ Test එකේ වැදගත්ම කොටස තමයි @AutoConfigureStubRunner annotation එක. මේකෙන් Stub Runner එක activate කරනවා. ids attribute එකට අපි Producer ගේ GroupId, ArtifactId, Version, Classifier, සහ Port එක දෙනවා. stubsMode = StubRunnerProperties.StubsMode.LOCAL කියන්නේ Stub Runner එක local Maven repository එකේ තියෙන Producer ගේ stubs jar එක පාවිච්චි කරනවා කියන එකයි. (Producer ගේ project එක ./gradlew publishToMavenLocal කරලා stubs jar එක publish කරලා තියෙන්න ඕනේ).

මේ Test එක run කරනකොට වෙන්නේ, Stub Runner එක book-service එකේ stubs jar එක load කරගෙන, ඒක temporary embedded server එකක run කරනවා. අපේ BookClient එක http://localhost:8090 (අපිට ඕන Port එකක් දෙන්න පුළුවන්) call කරනකොට, ඒක සැබෑ Book Service එකට යන්නේ නැතුව, Stub Runner එකෙන් run කරන stub එකට යනවා. Stub එක Contract එකට අනුව Response එකක් දෙනවා, ඒකෙන් අපේ client code එක හරියට වැඩ කරනවද කියලා verify කරන්න පුළුවන්.

වැදගත් පියවර:

  1. Book Service (Producer) Build කරන්න: book-service project එකේ ./gradlew build publishToMavenLocal command එක run කරන්න. මේකෙන් contract tests run වෙලා, stubs jar එක local Maven repository එකට publish වෙනවා.
  2. Order Service (Consumer) Test කරන්න: order-service project එකේ ./gradlew test command එක run කරන්න. දැන් Consumer test එක run වෙන්නේ Producer ගේ stub එකට එරෙහිවයි.

මේ ක්‍රියාවලිය සාර්ථක වුණොත්, ඔයා සාර්ථකව Consumer-Driven Contracts Spring Cloud Contract භාවිතයෙන් implement කරලා තියෙනවා!

ප්‍රතිලාභ සහ හොඳම ක්‍රියාකාරකම් (Benefits and Best Practices)

CDC ක්‍රියාත්මක කිරීමෙන් ලැබෙන ප්‍රතිලාභ ගොඩයි:

  • වේගවත් සහ විශ්වාසනීය Integration Tests: සැබෑ Microservices අතර සම්බන්ධයක් නොමැතිවම Integration flow එක test කරන්න පුළුවන්.
  • කණ්ඩායම් අතර සහයෝගීතාවය වැඩි දියුණු වෙනවා: Producer සහ Consumer කණ්ඩායම් දෙකටම පොදු Contract එකක් තිබීම නිසා වැරදි අවබෝධයන් අඩු වෙනවා.
  • API changes කලින්ම දැනගන්න පුළුවන්: Producer API එක වෙනස් කරනකොට, ඒක Contract එකට පටහැනි නම්, deploy කරන්න කලින්ම දැනගන්න පුළුවන්.
  • Microservices ස්වාධීනව deploy කරන්න පුළුවන්: Services අතර දැඩි යැපීම් අඩු වෙන නිසා, එක service එකක් update කළාම අනිත් service එක කැඩෙයි කියලා බය වෙන්න ඕන නැහැ.

හොඳම ක්‍රියාකාරකම් (Best Practices):

  • Contract Versioning: API එක වෙනස් වෙනකොට Contract එකත් version කරන්න. Semantic Versioning (e.g., v1, v2) පාවිච්චි කරන්න පුළුවන්.
  • Separate Contracts Repository: Contract files වෙනම Git repository එකක තියාගන්න පුළුවන්. එතකොට Producer සහ Consumer දෙගොල්ලන්ටම access කරන්න ලේසියි.
  • Keep Contracts Simple: Complex logic Contract එකට දාන්න එපා. එය API behaviour එකේ Essential parts විතරක් define කරන්න.
  • Don't over-specify: API response එකේ හැම field එකක්ම Contract එකට දාන්න අවශ්‍ය නැහැ. Consumer ට අවශ්‍ය කරන fields විතරක් specify කරන්න.
  • Use Regex when appropriate: Dynamic values test කරන්න අවශ්‍ය නම් regular expressions පාවිච්චි කරන්න.

නිගමනය

ඉතින්, Developer යාළුවනේ, මේ tutorial එකෙන් ඔයාලට Consumer-Driven Contracts (CDC) සහ Spring Cloud Contract කියන්නේ මොකක්ද, ඒවා Microservices වලට කොච්චර වැදගත්ද, සහ ප්‍රායෝගිකව implement කරන්නේ කොහොමද කියලා පැහැදිලි අවබෝධයක් ලැබෙන්න ඇති කියලා හිතනවා. මේක Microservices ලෝකයේදී ඔයාගේ Integration Testing nightmare එකට හොඳම විසඳුමක්. කණ්ඩායම් අතර සහයෝගීතාවය වැඩි කරලා, Software Development Process එක වේගවත් කරන්න මේකෙන් ලොකු උදව්වක් ලැබෙනවා.

මේ concept එක ඔයාගේ ඊළඟ Microservices Project එකේදී implement කරලා බලන්න. මොකද ඔයාලගේ අත්දැකීම්? පහළින් comment එකක් දාන්න! ඔයාලට මොනවා හරි ප්‍රශ්න තියෙනවා නම් ඒවත් අහන්න අමතක කරන්න එපා. අපි ලබන tutorial එකෙන් හමුවෙමු!