Belajar Backend Developer - Testing Backend
Episode 8 of 28

Belajar Backend Developer - Testing Backend

Membangun kepercayaan pada kode backend melalui empat lapis pengujian: unit test untuk logika murni, integration test untuk database, contract test untuk kontrak API, dan end-to-end test untuk alur lengkap

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

Pendahuluan

Setelah di episode 7 kalian membangun autentikasi, pertanyaan berikutnya adalah: bagaimana memastikan semua ini benar-benar bekerja — sekarang dan di masa depan? Jawabannya adalah testing. Kode tanpa test adalah kode yang hanya berfungsi "kemarin": perubahan kecil bisa merusak fitur tanpa ada yang menyadarinya sampai produksi.

Mengapa testing penting di backend? Karena backend adalah tempat logika bisnis dan uang berada — error di sini berdampak langsung. Test bukan pengeluaran waktu, melainkan investasi yang memungkinkan refactor tanpa rasa takut. Episode ini membangun empat lapis pengujian, dari unit hingga end-to-end, dengan contoh nyata pada API toko online kita.

Piramida Testing

100%

Piramida testing: banyak unit test (cepat, murah), sedikit integration test, dan paling sedikit e2e test (lambat, mahal). Semakin ke atas, test semakin mahal dan semakin sedikit jumlahnya. Kalian tidak butuh ribuan e2e test — kalian butuh ribuan unit test dan puluhan integration/e2e.

Unit Test

Unit test menguji satu fungsi/unit logika tanpa dependensi eksternal — database, jaringan, dan sistem lain di-mock. Contoh nyata: fungsi perhitungan diskon di logika order kita.

src/domain/pricing.ts
export function calculateSubtotal(items: { qty: number; unitPrice: number }[]) {
  return items.reduce((sum, it) => sum + it.qty * it.unitPrice, 0)
}
 
export function applyDiscount(subtotal: number, percent: number) {
  if (percent < 0 || percent > 100) throw new Error("INVALID_DISCOUNT")
  return Math.round(subtotal * (1 - percent / 100) * 100) / 100
}
src/domain/pricing.test.ts - unit test
import { describe, expect, it } from "vitest"
import { applyDiscount, calculateSubtotal } from "./pricing.js"
 
describe("calculateSubtotal", () => {
  it("menjumlahkan qty dikali harga", () => {
    expect(calculateSubtotal([
      { qty: 2, unitPrice: 50000 },
      { qty: 1, unitPrice: 100000 },
    ])).toBe(200000)
  })
})
 
describe("applyDiscount", () => {
  it("mengurangi subtotal sesuai persen", () => {
    expect(applyDiscount(200000, 10)).toBe(180000)
  })
 
  it("menolak persen di luar rentang", () => {
    expect(() => applyDiscount(100000, 150)).toThrow("INVALID_DISCOUNT")
  })
})

Unit test harus cepat (milidetik per test) dan menentukan — jika ada bug, test menunjukkan fungsi mana yang salah.

Integration Test

Integration test menguji kerjasama antar komponen nyata: misalnya service yang membaca/menulis database PostgreSQL, atau handler HTTP dengan framework. Di sini database dipakai sungguhan (test database terpisah), bukan di-mock.

src/routes/products.test.ts - integration test
import { beforeAll, afterAll, describe, expect, it } from "vitest"
import { app } from "../app.js"
import { resetDb } from "../test/db.js"
 
beforeAll(async () => {
  await resetDb()
})
 
afterAll(async () => {
  await resetDb()
})
 
describe("POST /v1/products", () => {
  it("membuat produk dan mengembalikan 201", async () => {
    const res = await app.inject({
      method: "POST",
      url: "/v1/products",
      payload: { name: "Keyboard", price: 250000, stock: 10 },
    })
 
    expect(res.statusCode).toBe(201)
    const body = res.json()
    expect(body.data).toMatchObject({ name: "Keyboard", price: 250000 })
  })
 
  it("menolak harga negatif dengan 400", async () => {
    const res = await app.inject({
      method: "POST",
      url: "/v1/products",
      payload: { name: "Kabel", price: -1, stock: 5 },
    })
 
    expect(res.statusCode).toBe(400)
    expect(res.json().error).toBe("VALIDATION_FAILED")
  })
})

Note

Integration test memakai database asli di environment test — jangan pernah mengetes terhadap database produksi. Sediakan database test terpisah yang di-reset sebelum dan sesudah test. Ini memastikan test kalian tidak saling mencemari dan bisa dijalankan berulang kali.

Contract Test

Contract test memastikan kontrak antara konsumen dan penyedia API tidak rusak. Ini kunci saat banyak tim/layanan bergantung pada API kita (relevan untuk microservices di episode 12). Dengan Pact, kontrak diuji dari sisi konsumen dan provider:

contract/customer-pact.ts - sisi konsumen
import { Pact } from "@pact-foundation/pact"
 
const provider = new Pact({
  consumer: "storefront-web",
  provider: "catalog-api",
  port: 8080,
})
 
it("storefront mengharapkan daftar produk", async () => {
  await provider.addInteraction({
    state: "ada produk di katalog",
    uponReceiving: "request daftar produk",
    withRequest: { method: "GET", path: "/v1/products", query: { limit: "10" } },
    willRespondWith: {
      status: 200,
      body: { data: [{ id: 1, name: "Keyboard" }], meta: { page: 1 } },
    },
  })
 
  const res = await fetch("http://localhost:8080/v1/products?limit=10")
  expect(res.status).toBe(200)
  const body = await res.json()
  expect(body.data).toBeInstanceOf(Array)
 
  await provider.verify()
})

Ketika catalog-api diuji sebagai provider dengan kontrak yang sama, perubahan yang merusak bentuk respons langsung terdeteksi — sebelum konsumen rusak di produksi.

End-to-End Test

E2E test menjalankan seluruh sistem — API, database, dan dependensi eksternal (yang di-stub) — mengalir seperti user nyata. Contoh: alur belanja penuh.

e2e/checkout.e2e.ts
import { describe, expect, it } from "vitest"
 
describe("Alur checkout", () => {
  it("login → lihat produk → buat order → lihat order", async () => {
    const login = await api.post("/v1/auth/login", {
      email: "buyer@shop.test",
      password: "test-password",
    })
    const token = login.body.data.token
 
    const products = await api.get("/v1/products?limit=1", token)
    const productId = products.body.data[0].id
 
    const order = await api.post("/v1/orders", {
      items: [{ productId, qty: 2 }],
    }, token)
    expect(order.status).toBe(201)
 
    const detail = await api.get(`/v1/orders/${order.body.data.id}`, token)
    expect(detail.body.data.status).toBe("pending")
    expect(detail.body.data.total).toBeGreaterThan(0)
  })
})

E2E test lambat dan mahal — itu sebabnya jumlahnya sedikit dan hanya mencakup alur kritis (login, checkout, pembayaran), bukan setiap kombinasi.

Test Coverage dan Kebiasaan

Coverage adalah alat bantu, bukan tujuan. Target sehat untuk backend: 80%+ line coverage pada logika domain, dengan prioritas pada fungsi yang paling kritis (harga, stok, auth). Jalankan:

Jalankan test dengan coverage
bun run test
bun run test:coverage

Kebiasaan yang membuat testing efektif:

  • Tulis test untuk bug yang pernah terjadi — sebelum memperbaiki, tulis test yang gagal, baru perbaiki (TDD untuk regresi).
  • Test harus independen dan bisa dijalankan ulang — tidak bergantung urutan.
  • Jangan test implementasi, test perilaku — refactor internal tidak boleh memaksa test berubah.

Common Pitfalls

Testing Selalu Memakai Mock

Mock berlebihan membuat test hanya menguji implementasi, bukan perilaku. Mock untuk batas sistem (jaringan, payment), bukan untuk logika internal.

Test Lambat Menjadi Beban

Jika test suite butuh 30 menit, developer tidak menjalankannya. Jaga unit test tetap cepat; naikkan e2e ke pipeline CI yang berjalan saat merge.

Flaky Test

Test yang kadang lulus kadang gagal tanpa alasan jelas — biasanya karena dependensi waktu (timeout, date) atau database yang tidak bersih. Flaky test lebih buruk daripada tidak ada test karena merusak kepercayaan.

Tip

Mulailah dari lapisan bawah piramida: pastikan unit test pada logika domain (harga, stok, auth) lengkap dulu, tambah integration test untuk CRUD database, lalu e2e untuk alur kritis. Rasionya yang sehat di tim kecil: ratusan unit, puluhan integration, 5-10 e2e.

Penutup

Episode 8 membangun empat lapis pengujian: unit test untuk logika murni, integration test dengan database nyata, contract test untuk kontrak API, dan e2e test untuk alur kritis — diorganisir sebagai piramida.

Inti yang harus dibawa pulang:

  • Piramida testing: banyak unit, sedang integration, sedikit e2e.
  • Unit test cepat dan terisolasi; jangan mock berlebihan.
  • Integration test memakai database test terpisah, bukan produksi.
  • Contract test melindungi kontrak API antar layanan/tim.
  • E2E test hanya untuk alur kritis karena mahal.

Di episode 9 selanjutnya kita akan mempercepat API: caching & performance — Redis cache, CDN, cache strategy, dan optimasi query untuk menurunkan latency. Sampai jumpa di episode 9!

Belajar Backend Developer - Testing Backend | Belajar Backend