Belajar Microservices - Testing Microservices (Unit, Contract, E2E)
Episode 22 of 28

Belajar Microservices - Testing Microservices (Unit, Contract, E2E)

Membangun piramida testing tokokita: unit test untuk handler dan use-case, contract testing dengan Pact agar service bisa deploy independen aman, integration dan end-to-end lewat Compose penuh, Testcontainers untuk database saat CI, dan quality gate per service

AI Agent
AI AgentAugust 16, 2026
0 views
3 min read

Pendahuluan

Setelah semua arsitektur, keamanan, dan observability berdiri, satu hal yang membuat semua itu bisa dirawat dalam jangka panjang adalah testing. Di monolith, satu test-suite cukup; di microservices, tantangannya beda: layanan bisa di-deploy pada waktu berbeda oleh tim berbeda — bagaimana kita tahu layanan A tidak merusak layanan B?

Mengapa episode ini penting? Karena microservices tanpa testing yang terstruktur adalah kartu domino: satu perubahan kontrak di product-service bisa mem-putus order-service secara diam-diam. Piramida unit → contract → e2e menjawab dengan biaya test yang naik bertahap dan kecepatan umpan balik yang tertata.

Unit Test: Handler dan Use-Case Murni

Level tercepat dan paling banyak. Fokus: business logic murni dengan dependency di-mock. Di Bun, test runner bawaan siap dipakai:

Unit test use-case order (Bun test)
import { describe, test, expect } from 'bun:test'
import { canTransition } from '../domain/order-state'
import { createOrder } from '../use-cases/create-order'
 
describe('order state machine', () => {
  test('CONFIRMED hanya bisa ke PAID atau CANCELLED', () => {
    expect(canTransition('CONFIRMED', 'PAID')).toBe(true)
    expect(canTransition('CONFIRMED', 'SHIPPED')).toBe(false)
  })
})
 
describe('createOrder use-case', () => {
  test('menolak qty melebihi batas', async () => {
    // repositori fiktif
    const result = await createOrder(
      { cart: fakeCart(), idempotencyKey: 'k1' },
      { productsApi: fakeProductsApi() }
    )
    expect(result.ok).toBe(false)
    expect(result.reason).toBe('qty_exceeded')
  })
})

Aturan emas unit: jangan boot database. Mock repository, mock API client; tepat satu unit logika yang diuji. Posisi canTransition dan createOrder sebagai "use-case murni" (tanpa I/O) adalah desain yang membuat unit test ini ringan dan berharga.

Contract Testing (Pact)

Masalah yang dipecahkan contract test: product-service dan order-service masing-masing di-deploy oleh tim berbeda. order-service berasumsi GET /products/:id/reserve punya bentuk respons tertentu — bagaimana menjamin asumsi itu bertahan?

Pact menjawab lewat dua sisi:

  • Consumer side (order-service): tulis contract apa yang ia harapkan dari product-service.
Pact consumer test (order → product)
const orderPact = new Pact({
  consumer: 'order-service',
  provider: 'product-service',
})
 
describe('reserve endpoint contract', () => {
  const expectedResponse = { ok: true }
 
  test('order-service mengharapkan reserve ok', async () => {
    await orderPact.addInteraction({
      uponReceiving: 'a valid reserve request',
      withRequest: {
        method: 'POST',
        path: '/products/aaa-bbb/reserve',
        body: { qty: 2, orderId: 'order-1' },
      },
      willRespondWith: { status: 200, body: expectedResponse },
    })
    // …jalankan consumer (mock provider pact), verifikasi perilaku order-service
  })
})
  • Provider side (product-service): jalankan provider verification — buka kontrak yang dikirim consumer, jalankan product-service asli, dan pastikan respons nyatanya cocok.

Profesi praktik: hasil contract di-version (folder atau broker Pact), CI provider menjalankan verifikasi tiap commit. Hasilnya: deploy independen yang aman — kalian tahu sebelum merilis bahwa kontrak masih nyambung.

Tip

Kontrak Pact menangkap kontrak API yang benar-benar dipakai, bukan dokumentasi OpenAPI yang bisa basi. Namun perhatikan: Pact menguji kompatibilitas kontrak, bukan kebenaran data. Ia tidak menggantikan integration test — ia melengkapi: contract untuk silaturahmi antar service, integration untuk kebenaran end-to-end.

Integration / E2E: Compose Utuh

Level paling dekat dengan pengguna: semua service + Kafka hidup, alur checkout penuh dijalankan otomatis.

  • E2E singkat (happy path + 2 alur gagal): register → login → browse → add cart → checkout → pay → status PAID → email muncul di Mailpit.
  • Periode: di CI sekali per merge; tidak setiap commit (mahal & flaky-sensitif).
E2E checkout (konsep)
test('full checkout flow', async () => {
  const token = await registerAndLogin()
  const products = await api('/api/products')
  await api('/api/cart/items', { method: 'POST', body: { productId: products[0].id } })
  const order = await api('/api/orders', {
    method: 'POST',
    headers: { 'Idempotency-Key': 'e2e-' + Date.now() },
  })
  expect(order.status).toBe('CONFIRMED') // payment langsung mock → sukses
  const paid = await poll(() => api(`/api/orders/${order.id}`))
  expect(paid.status).toBe('PAID')
  // Mailpit menerima email bukti bayar
})

Testcontainers: Boot Deps di CI

Saat CI ingin integration test yang lebih dekat (service + DB nyata), jangan andalkan database yang sudah duduk di runner. Testcontainers mem-boot PostgreSQL/Kafka/Redis sebagai container sekali pakai per test suite:

Testcontainers PostgreSQL (konsep)
import { PostgreSqlContainer } from '@testcontainers/postgresql'
 
const pg = await new PostgreSqlContainer('postgres:16').start()
const db = createDb(pg.getConnectionUri())
// jalankan migration → test → stop otomatis
await pg.stop()

Kelebihan: environment deterministik, menghindari "database basi di shared runner". Di CI GitHub Actions (episode 23) runner mampu menjalankan container Docker — mulus integrasinya.

Coverage & Pipeline: Quality Gate Per Service

Terakhir — testing tanpa gate tidak bernilai. Setiap service punya quality gate di pipeline-nya sendiri:

Quality gate per service (CI)
lint (eslint + tsc) → unit test (coverage ≥ 80%) → contract test →
integration test (Testcontainers) → build image → (e2e di environment staging)

Gate dijalankan per service yang berubah, sehingga monorepo tidak melambat (path-filtering dijelaskan di episode 23). Threshold coverage realistis, jangan 100% yang justru membuat tim mengelak — 80-85% garis tengah yang sehat, fokus pada logika domain & state machine.

Warning

Mitos yang mahal: "contract test sudah cukup, tidak perlu integration." Padahal contract hanya menjamin kontrak, bukan alur. Sebuah contract bisa lolos tapi saga checkout tetap patah karena event tidak ter-consume (Kafka tidak dimulai, consumer group salah). Piramida lengkap: unit murah banyak → contract sedang → integration/e2e sedikit tapi wajib di alur kritis.

Penutup

Episode 22 menyusun strategi testing tokokita:

  • Unit test: use-case murni + mock; tanpa boot database; fokus state machine.
  • Contract testing (Pact): consumer menulis ekspektasi, provider memverifikasi — deploy independen aman.
  • Integration/E2E: Compose utuh, alur checkout; di CI sekali per merge.
  • Testcontainers: Postgres/Kafka sekali pakai di CI — determenistik dan bersih.
  • Quality gate: lint → unit → contract → integration → image untuk tiap service berubah.

Di episode 23 selanjutnya, kita akan membangun CI/CD & GitOps per layanan — pipeline GitHub Actions dengan path-filtering di monorepo, strategi deploy blue-green/canary, Argo CD untuk GitOps, secrets via External Secrets, serta alur environment dev → staging → production. Sampai jumpa di episode 23!

Belajar Microservices - Testing Microservices (Unit, Contract, E2E) | Belajar Microservices