Apa itu Contract Testing?
Contract testing memastikan bahwa dua sistem (misalnya frontend dan backend) sepakat tentang bentuk data yang dikirim dan diterima. Alih-alih menguji integrasi secara langsung, kita menguji "kontrak" — spesifikasi API yang disepakati kedua pihak.
Mengapa Perlu Contract Testing?
- API berubah tanpa pemberitahuan — field dihapus, tipe berubah, format response beda
- Integration test terlalu lambat — menjalankan kedua sistem sekaligus mahal
- Microservices — semakin banyak service, semakin sulit menjaga kompatibilitas
Consumer-Driven Contracts
Pendekatan di mana consumer (frontend/client) mendefinisikan apa yang dibutuhkan, lalu provider (backend/API) memverifikasi bahwa kontrak tersebut terpenuhi:
// Consumer (Frontend) mendefinisikan kontrak:
// "Saya butuh GET /api/users/1 mengembalikan { id, name, email }"
// Provider (Backend) memverifikasi:
// "Apakah endpoint saya memenuhi kontrak ini?"
Pact — Framework Contract Testing
Pact adalah framework populer untuk consumer-driven contract testing. Workflow-nya:
- Consumer menulis test yang mendefinisikan interaksi yang diharapkan
- Pact menghasilkan file
pact.jsonberisi kontrak - Provider menjalankan verifikasi terhadap file pact tersebut
- Jika provider memenuhi semua kontrak, test lulus
Schema Validation dengan Zod
Cara yang lebih ringan: gunakan library validasi schema untuk mengecek API response di test:
import { z } from "zod";
// Definisikan schema (kontrak)
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
role: z.enum(["admin", "user"]),
createdAt: z.string().datetime(),
});
type User = z.infer<typeof UserSchema>;
// Test bahwa API response sesuai kontrak
it("GET /api/users/1 sesuai schema", async () => {
const response = await fetch("/api/users/1");
const data = await response.json();
// Jika data tidak sesuai schema, Zod melempar error
const parsed = UserSchema.parse(data);
expect(parsed.id).toBe(1);
expect(parsed.name).toBeDefined();
});
Schema Validation dengan Joi
import Joi from "joi";
const userSchema = Joi.object({
id: Joi.number().required(),
name: Joi.string().required(),
email: Joi.string().email().required(),
role: Joi.string().valid("admin", "user").required(),
});
it("response sesuai kontrak", async () => {
const data = await fetchUser(1);
const { error } = userSchema.validate(data);
expect(error).toBeUndefined();
});
Testing API Response Shape
Tanpa library validasi, kita bisa mengecek bentuk response secara manual:
it("response memiliki properti yang diharapkan", async () => {
const response = await fetch("/api/users/1");
const user = await response.json();
// Cek structure, bukan value spesifik
expect(user).toMatchObject({
id: expect.any(Number),
name: expect.any(String),
email: expect.stringMatching(/@/),
role: expect.stringMatching(/^(admin|user)$/),
});
// Pastikan tidak ada field yang hilang
expect(Object.keys(user)).toEqual(
expect.arrayContaining(["id", "name", "email", "role"])
);
});
Best Practices
- Simpan schema sebagai single source of truth — share antara frontend dan backend
- Gunakan Zod atau Joi di runtime juga — tidak hanya di test, tapi juga saat menerima data
- Versioning API — saat kontrak berubah, buat versi baru agar consumer lama tidak rusak
- Jalankan contract test di CI — gagalkan deployment jika kontrak dilanggar