CONTRACT-TESTING MICROSERVICES API-TESTING INTEGRATION-TESTING DEVOPS SOFTWARE-TESTING API-DEVELOPMENT BACKEND CI/CD RELIABILITY DISTRIBUTED-SYSTEMS

Contract Testing untuk Microservices: Membangun Integrasi API yang Andal dan Bebas Drama

⏱️ 11 menit baca
👨‍💻

Contract Testing untuk Microservices: Membangun Integrasi API yang Andal dan Bebas Drama

1. Pendahuluan

Di era arsitektur microservices, aplikasi kita tidak lagi berdiri sendiri. Mereka adalah orkestra layanan-layanan kecil yang bekerja sama, berkomunikasi satu sama lain melalui API. Ini membawa banyak keuntungan seperti skalabilitas dan fleksibilitas, tapi juga tantangan besar: bagaimana kita memastikan semua layanan ini selalu “berbicara” dalam bahasa yang sama dan kompatibel?

Bayangkan skenario ini: Tim A mengembangkan layanan User yang menyediakan data pengguna, sementara Tim B mengembangkan layanan Order yang membutuhkan data pengguna dari layanan User. Tim A melakukan perubahan kecil pada response API layanan User mereka. Mereka yakin itu backward-compatible. Tapi, tiba-tiba, setelah deployment, layanan Order milik Tim B mengalami error fatal di produksi karena payload yang mereka harapkan berubah. Panik!

Masalah seperti ini sering terjadi di lingkungan microservices. Integration tests tradisional yang menguji seluruh sistem secara end-to-end memang bisa menangkapnya, tapi seringkali lambat, kompleks, dan sulit dikelola seiring bertambahnya jumlah layanan. Di sinilah Contract Testing hadir sebagai penyelamat.

Dalam artikel ini, kita akan menyelami apa itu Contract Testing, mengapa ia sangat penting untuk microservices, dan bagaimana Anda bisa mengimplementasikannya untuk membangun integrasi API yang lebih andal dan bebas drama.

2. Apa Itu Contract Testing?

📌 Contract Testing adalah pendekatan testing yang memastikan dua aplikasi (atau lebih) dapat berkomunikasi satu sama lain. Alih-alih menguji seluruh sistem secara end-to-end, Contract Testing fokus pada “kontrak” komunikasi antara Consumer (aplikasi yang memanggil API) dan Provider (aplikasi yang menyediakan API).

Analoginya begini: Anda ingin membeli kopi di sebuah kafe. Anda (Consumer) mengharapkan kafe tersebut (Provider) bisa menyediakan “Kopi Latte” dengan gula terpisah. Anda tidak perlu tahu bagaimana kafe itu menanam biji kopi, memanggangnya, atau bahkan bagaimana mesin kopinya bekerja. Yang penting, kafe itu bisa memenuhi “kontrak” Anda: menyediakan Kopi Latte dengan gula terpisah.

Jika kafe itu tiba-tiba hanya menyediakan “Kopi Hitam” atau “Kopi Latte” tanpa gula, kontrak Anda rusak. Contract Testing membantu memastikan “kontrak” ini selalu terpenuhi.

Consumer-Driven Contracts (CDC)

Pendekatan paling populer dalam Contract Testing adalah Consumer-Driven Contracts (CDC). Seperti namanya, kontrak ini “didikte” oleh consumer.

Alur CDC:

  1. Consumer menulis test yang mendefinisikan ekspektasinya terhadap API provider. Ini adalah “kontrak” mereka.
  2. Test ini menghasilkan sebuah file Pact (sering disebut contract file) yang berisi detail ekspektasi consumer.
  3. Pact ini kemudian diberikan kepada Provider.
  4. Provider menjalankan test sendiri menggunakan file Pact tersebut untuk memverifikasi bahwa API mereka benar-benar memenuhi semua ekspektasi consumer.

Jika provider mengubah API-nya sehingga tidak lagi memenuhi kontrak consumer, test provider akan gagal, dan mereka akan tahu adanya breaking change sebelum deployment. Ini mencegah kejutan tidak menyenangkan di produksi!

3. Mengapa Contract Testing Penting untuk Microservices?

Di lingkungan microservices yang dinamis, perubahan adalah hal yang konstan. Contract Testing menawarkan beberapa manfaat krusial:

Masalah Tanpa Contract Testing:

Dengan Contract Testing:

4. Bagaimana Cara Kerja Contract Testing dengan Pact?

💡 Pact adalah salah satu tool Contract Testing paling populer yang mengimplementasikan konsep Consumer-Driven Contracts. Mari kita lihat alurnya dengan Pact.

Langkah 1: Consumer Menulis Test dan Menghasilkan Pact File

Sebagai consumer, Anda akan menulis unit test yang menggunakan mock dari provider Anda. Mock ini akan merekam semua permintaan yang Anda buat dan respons yang Anda harapkan.

Contoh Sederhana (Node.js dengan Pact)

Misalkan layanan Order Anda (consumer) memanggil layanan User (provider) untuk mendapatkan detail pengguna berdasarkan ID.

// consumer.test.js
const { Verifier } = require("@pact-foundation/pact");
const { someApiCall } = require("./consumer-client"); // Klien yang memanggil API User

describe("Consumer Test with Pact", () => {
  let provider;

  beforeAll(async () => {
    provider = new Verifier({
      providerBaseUrl: "http://localhost:8080", // URL service provider yang akan diuji
      pactUrls: [
        "http://localhost:9292/pacts/provider/UserAPI/consumer/OrderService/latest",
      ], // URL Pact Broker
      // ... konfigurasi lainnya
    });
    // Ini adalah contoh consumer-side test, yang sebenarnya menghasilkan pact file
    // Untuk contoh ini, kita akan fokus pada alur verifikasi
  });

  it("should get user details from UserAPI", async () => {
    // Pada tahap ini, consumer akan menulis test yang memanggil mock provider
    // dan mock tersebut akan merekam interaksi ke dalam pact file.
    // Misal, menggunakan pact.js untuk membuat mock server:
    // const { Pact } = require('@pact-foundation/pact');
    // const provider = new Pact({ consumer: 'OrderService', provider: 'UserAPI', port: 8080 });
    // await provider.setup();
    // await provider.addInteraction({
    //   state: 'a user with ID 1 exists',
    //   uponReceiving: 'a request for user details',
    //   withRequest: {
    //     method: 'GET',
    //     path: '/users/1',
    //     headers: { 'Accept': 'application/json' },
    //   },
    //   willRespondWith: {
    //     status: 200,
    //     headers: { 'Content-Type': 'application/json' },
    //     body: { id: 1, name: 'Budi' },
    //   },
    // });
    // await someApiCall(1); // Panggil fungsi klien yang akan memicu request
    // await provider.verify();
    // await provider.finalize();
  });
});

Setelah consumer test dijalankan, Pact akan menghasilkan file pact.json yang berisi “kontrak” ini. File ini kemudian di-publish ke Pact Broker (semacam repository untuk pact files).

// Contoh isi pact.json (disederhanakan)
{
  "consumer": { "name": "OrderService" },
  "provider": { "name": "UserAPI" },
  "interactions": [
    {
      "description": "a request for user details",
      "request": {
        "method": "GET",
        "path": "/users/1",
        "headers": { "Accept": "application/json" }
      },
      "response": {
        "status": 200,
        "headers": { "Content-Type": "application/json" },
        "body": { "id": 1, "name": "Budi" }
      },
      "providerStates": [{ "name": "a user with ID 1 exists" }]
    }
  ],
  "metadata": {
    /* ... */
  }
}

Langkah 2: Provider Memverifikasi Pact File

Sekarang, giliran provider (layanan User) untuk menguji apakah API mereka memenuhi kontrak yang ada di pact.json.

// provider.test.js
const { Verifier } = require("@pact-foundation/pact");
const path = require("path");
const { startServer } = require("./user-service"); // Fungsi untuk menjalankan service User

describe("Provider Test with Pact", () => {
  let server;

  beforeAll(async () => {
    // Jalankan service User di port tertentu
    server = await startServer(8080);
  });

  afterAll(() => {
    server.close();
  });

  it("should validate the expectations of the OrderService consumer", () => {
    const opts = {
      providerBaseUrl: "http://localhost:8080",
      pactUrls: [
        path.resolve(process.cwd(), "./pacts/orderservice-userapi.json"),
      ], // Atau dari Pact Broker
      providerStatesSetupUrl: "http://localhost:8080/pact-setup", // Endpoint untuk menyiapkan state provider
      logLevel: "DEBUG",
    };

    return new Verifier(opts)
      .verifyProvider()
      .then(() => {
        console.log("Pact Verification Complete!");
      })
      .catch((error) => {
        console.error("Pact Verification Failed:", error);
        throw error;
      });
  });
});

// Contoh user-service.js (Express)
const express = require("express");
const app = express();

app.get("/users/:id", (req, res) => {
  const userId = parseInt(req.params.id);
  if (userId === 1) {
    res.status(200).json({ id: 1, name: "Budi" });
  } else {
    res.status(404).json({ message: "User not found" });
  }
});

// Endpoint untuk setup provider state (penting untuk Pact)
app.post("/pact-setup", (req, res) => {
  const { state } = req.body;
  if (state === "a user with ID 1 exists") {
    // Lakukan setup database atau mock internal agar user ID 1 ada
    console.log("Provider state setup: a user with ID 1 exists");
    res.status(200).send();
  } else {
    res.status(400).send("Unknown state");
  }
});

function startServer(port) {
  return new Promise((resolve) => {
    const s = app.listen(port, () => {
      console.log(`User Service running on port ${port}`);
      resolve(s);
    });
  });
}

Jika ada ketidakcocokan antara ekspektasi di pact.json dan response API provider yang sebenarnya, test ini akan gagal. Provider akan tahu persis apa yang rusak dan dapat memperbaikinya sebelum deployment.

🎯 Pact Broker: Pact Broker adalah server yang menyimpan semua pact files. Ini memungkinkan consumer dan provider untuk berbagi kontrak dengan mudah dan menyediakan gambaran visual tentang kompatibilitas layanan.

5. Implementasi Praktis & Best Practices

⚠️ Kapan Tidak Menggunakan Contract Testing?

Kesimpulan

Contract Testing, khususnya dengan pendekatan Consumer-Driven Contracts menggunakan tool seperti Pact, adalah strategi testing yang sangat kuat untuk arsitektur microservices. Ini memungkinkan tim untuk bekerja secara mandiri dengan percaya diri, mendeteksi breaking changes lebih awal, dan pada akhirnya, membangun sistem terdistribusi yang lebih andal dan tangguh.

Dengan mengadopsi Contract Testing, Anda tidak hanya menguji kode, tetapi juga membangun budaya kolaborasi dan kepercayaan antar tim. Jadi, jika Anda sering mengalami drama integrasi di lingkungan microservices Anda, Contract Testing mungkin adalah solusi yang Anda cari. Selamat mencoba!

🔗 Baca Juga