Contract testing GraphQL dipakai untuk memastikan perubahan skema tidak merusak client yang sudah bergantung pada kontrak API saat ini. Intinya sederhana: setiap perubahan schema harus divalidasi terhadap query, mutation, dan fragment yang benar-benar digunakan client, lalu pipeline CI menolak perubahan yang memperkenalkan breaking change.

Masalah yang paling sering muncul pada GraphQL bukan hanya field yang dihapus. Regresi juga sering datang dari perubahan nullability, pengetatan input, modifikasi enum, atau field yang sudah deprecated tetapi ternyata masih dipakai. Jika hanya mengandalkan integration test end-to-end, banyak perubahan skema bisa lolos karena coverage query client tidak lengkap. Di sinilah contract testing lebih tepat sasaran.

Apa yang Dimaksud Contract Testing pada GraphQL

Pada konteks GraphQL, kontrak biasanya terdiri dari dua sisi:

  • Provider contract: schema GraphQL yang dipublikasikan server.
  • Consumer contract: operasi GraphQL yang dipakai client, seperti query, mutation, dan fragment.

Contract testing memeriksa apakah schema baru masih kompatibel dengan operasi client yang ada. Pemeriksaan ini berbeda dari sekadar menjalankan test resolver. Fokusnya bukan hanya apakah server mengembalikan respons, tetapi apakah bentuk kontrak tetap valid untuk consumer.

Pendekatan ini sangat berguna saat:

  • beberapa frontend memakai API yang sama,
  • backend berubah lebih cepat daripada siklus rilis client,
  • tim ingin menahan breaking change sebelum merge,
  • schema dikelola sebagai artefak yang ditinjau di CI.

Jenis Breaking Change GraphQL yang Paling Sering Menyebabkan Regresi

1. Field dihapus atau tipe field diubah

Ini bentuk regresi paling jelas. Jika client masih meminta field yang sudah dihapus, validasi query akan gagal. Perubahan tipe juga bisa memutus kontrak jika client mengharapkan struktur tertentu.

type Product {
  id: ID!
  name: String!
  price: Int!
}

Jika price dihapus atau diganti menjadi objek lain tanpa migrasi yang jelas, client lama akan rusak.

2. Perubahan nullability

Nullability sering dianggap detail kecil, padahal efeknya besar. Mengubah field output dari nullable ke non-null atau sebaliknya dapat memengaruhi validasi, kode generator, dan asumsi logika di client.

Contoh:

type User {
  email: String
}

Lalu diubah menjadi:

type User {
  email: String!
}

Secara teori, ini bisa tampak aman untuk consumer karena field menjadi lebih ketat. Namun implementasi resolver dan alur data harus benar-benar menjamin nilai tidak pernah null. Jika tidak, server dapat gagal pada runtime.

Sebaliknya, perubahan pada input argument dari opsional menjadi wajib hampir selalu merupakan breaking change bagi client lama.

type Query {
  user(id: ID): User
}

Diubah menjadi:

type Query {
  user(id: ID!): User
}

Query client lama yang tidak mengirim argumen akan langsung tidak valid.

3. Enum diubah

Enum terlihat stabil, tetapi perubahan nilainya sangat sensitif. Menghapus enum value yang masih dipakai client adalah breaking change. Menambah enum value sering tidak merusak validasi query, tetapi bisa merusak logika client jika kode mengasumsikan daftar nilai selalu tetap, misalnya pada switch-case tanpa default.

enum OrderStatus {
  PENDING
  PAID
  CANCELED
}

Jika CANCELED dihapus, operasi client yang memakai nilai itu pada mutation atau filter akan gagal.

4. Input type diperketat

Perubahan pada input object sering memicu regresi yang tidak langsung terlihat. Contohnya:

  • menambah field wajib baru pada input object,
  • mengubah tipe field input,
  • menghapus field input yang masih dikirim client.
input UpdateProfileInput {
  displayName: String
  bio: String
}

Jika diubah menjadi:

input UpdateProfileInput {
  displayName: String!
  bio: String
}

Client lama yang hanya mengirim bio akan gagal.

5. Field deprecated yang ternyata masih dipakai

@deprecated bukan mekanisme penghapusan otomatis. Banyak tim menandai field deprecated, tetapi menghapusnya terlalu cepat tanpa bukti bahwa semua consumer sudah migrasi. Ini sumber regresi yang sangat umum.

type Product {
  id: ID!
  title: String! @deprecated(reason: "Gunakan name")
  name: String!
}

Selama masih ada query yang meminta title, field tersebut belum aman dihapus. Contract testing membantu memastikan status pemakaian aktual, bukan asumsi.

Contoh Sederhana: Schema, Query Client, dan Titik Regresi

Schema awal

type Query {
  product(id: ID!): Product
}

type Product {
  id: ID!
  name: String!
  title: String @deprecated(reason: "Gunakan name")
  stock: Int
  status: ProductStatus!
}

enum ProductStatus {
  ACTIVE
  ARCHIVED
}

Query client yang saat ini dipakai

query ProductDetail($id: ID!) {
  product(id: $id) {
    id
    title
    stock
    status
  }
}

Misalkan backend mengusulkan schema baru berikut:

type Query {
  product(id: ID!): Product
}

type Product {
  id: ID!
  name: String!
  stock: Int!
  status: ProductStatus!
}

enum ProductStatus {
  ACTIVE
}

Dari sudut pandang backend, perubahan ini mungkin terlihat wajar: field deprecated dihapus, stock dipastikan selalu ada, dan enum disederhanakan. Tetapi bagi client di atas, ada dua risiko nyata:

  • title sudah dihapus, sehingga query lama tidak lagi valid.
  • Penghapusan ARCHIVED bisa memutus alur lain yang mengirim atau mengandalkan nilai tersebut.

Contract test akan menangkap perubahan ini sebelum merge, tanpa harus menunggu bug muncul di frontend.

Workflow Contract Testing GraphQL di CI

Workflow yang efektif biasanya terdiri dari tiga lapisan verifikasi: schema diff, validasi operasi client, dan merge gate.

1. Ekspor atau publikasikan schema sebagai artefak

Pipeline perlu memiliki representasi schema yang bisa dibandingkan secara konsisten. Umumnya tim menyimpan hasil introspection atau SDL schema di repository, artifact build, atau registry schema. Yang penting, ada baseline yang jelas untuk dibandingkan.

Praktik yang umum:

  • simpan schema stabil terakhir dari branch utama,
  • hasilkan schema kandidat dari branch fitur,
  • bandingkan keduanya di CI.

2. Jalankan schema diff untuk mendeteksi breaking change

Gunakan alat diff schema GraphQL yang umum dipakai untuk mengklasifikasikan perubahan menjadi aman, berisiko, atau breaking. Tujuannya bukan hanya melihat teks berubah, tetapi memahami dampak kontraknya.

Hal yang sebaiknya dideteksi sebagai minimal:

  • field/type dihapus,
  • argument menjadi wajib,
  • input field menjadi wajib,
  • enum value dihapus,
  • signature field berubah.

Jika alat Anda mendukung klasifikasi perubahan, gunakan hasil itu sebagai syarat pipeline. Jika tidak, paling tidak pastikan perubahan schema ditinjau sebagai artefak yang terlihat jelas di pull request.

3. Validasi query dan mutation client terhadap schema baru

Ini lapisan yang paling penting. Schema diff memberi sinyal perubahan, tetapi validasi operasi client memberi bukti apakah perubahan itu benar-benar memutus consumer yang ada.

Sumber operasi client dapat diambil dari:

  • dokumen .graphql di repository frontend,
  • operasi yang dikumpulkan dari build aplikasi,
  • persisted queries jika arsitektur Anda memakainya,
  • repositori terpisah untuk beberapa consumer.

Prosesnya sederhana:

  1. kumpulkan semua query, mutation, dan fragment yang dianggap kontrak aktif,
  2. kompilasi atau validasi dokumen tersebut terhadap schema kandidat,
  3. gagalkan pipeline jika ada operasi yang tidak valid.

4. Tambahkan gate sebelum merge

CI baru efektif jika hasil validasi benar-benar menjadi penghambat merge. Idealnya pull request tidak bisa digabung bila:

  • schema diff mengandung breaking change yang tidak disetujui,
  • ada operasi client yang gagal divalidasi,
  • field deprecated akan dihapus tetapi masih terdeteksi dipakai consumer.

Pada tahap ini, tim biasanya menambahkan mekanisme pengecualian yang terdokumentasi, misalnya untuk migrasi terencana. Namun pengecualian harus eksplisit, bukan diam-diam melonggarkan aturan.

Contoh Implementasi Praktis yang Netral terhadap Tool

Berikut contoh alur yang bisa diterapkan dengan alat GraphQL yang umum, tanpa bergantung pada library tertentu.

Struktur berkas

/schema
  current-schema.graphql
  next-schema.graphql
/clients/web/queries
  product-detail.graphql
  product-list.graphql
/clients/mobile/queries
  product-detail.graphql
/scripts
  check-schema-diff.sh
  validate-operations.sh

Contoh query client

query ProductDetail($id: ID!) {
  product(id: $id) {
    id
    title
    stock
    status
  }
}

Langkah CI secara konseptual

# 1. Generate atau ambil schema kandidat dari branch saat ini
./scripts/export-schema.sh > schema/next-schema.graphql

# 2. Bandingkan dengan schema acuan
./scripts/check-schema-diff.sh schema/current-schema.graphql schema/next-schema.graphql

# 3. Validasi seluruh operasi client terhadap schema baru
./scripts/validate-operations.sh schema/next-schema.graphql ./clients/**/queries/*.graphql

Implementasi skrip bisa memakai CLI dari ekosistem GraphQL yang umum untuk:

  • membaca SDL atau hasil introspection,
  • membandingkan dua schema,
  • memvalidasi dokumen GraphQL terhadap schema.

Prinsip pentingnya bukan nama tool, melainkan hasil yang ingin dicapai:

  • perubahan skema terdeteksi otomatis,
  • dampaknya terhadap operasi nyata terlihat jelas,
  • pipeline gagal sebelum merge jika kontrak rusak.

Contoh logika validasi field deprecated

Selain mengecek query valid atau tidak, banyak tim menambahkan aturan tambahan seperti:

  • peringatan jika query masih memakai field deprecated,
  • gagal build jika field deprecated akan dihapus tetapi usage masih ada,
  • laporan consumer mana yang belum migrasi.

Ini penting karena field deprecated biasanya tidak merusak validasi sampai akhirnya dihapus. Jika menunggu sampai penghapusan, Anda terlambat.

Strategi Adopsi yang Realistis

Mulai dari operasi yang benar-benar dipakai

Kesalahan umum adalah mencoba memodelkan semua kemungkinan query. Fokuslah pada operasi yang ada di kode produksi atau persisted queries. Contract testing paling efektif bila dataset operasinya akurat.

Jadikan schema sebagai artefak yang ditinjau

Jangan biarkan perubahan schema tersembunyi di balik perubahan resolver. Simpan hasil schema dan tampilkan diff di pull request agar reviewer backend maupun frontend bisa melihat dampaknya.

Gabungkan dengan kebijakan deprecation

Deprecation harus punya proses, bukan hanya anotasi. Contoh kebijakan yang masuk akal:

  1. tandai field deprecated dan umumkan pengganti,
  2. pantau penggunaan oleh consumer,
  3. hapus hanya setelah tidak ada operasi aktif yang memakainya.

Kelola consumer lintas repository

Jika frontend dan backend berada di repository berbeda, buat mekanisme untuk mengumpulkan operasi client ke pipeline backend atau gunakan registry terpusat. Tanpa itu, contract test hanya memverifikasi sebagian consumer.

Checklist Adopsi Contract Testing GraphQL

  • Schema server dapat diekspor secara konsisten di CI.
  • Ada baseline schema dari branch utama atau rilis terakhir.
  • Dokumen query/mutation/fragment client terkumpul dan dapat divalidasi otomatis.
  • Schema diff dijalankan pada setiap pull request yang mengubah API.
  • Validasi operasi client dijalankan terhadap schema kandidat.
  • Pipeline gagal untuk breaking change yang tidak diizinkan.
  • Field deprecated dipantau penggunaannya sebelum dihapus.
  • Ada proses pengecualian untuk migrasi besar, dengan persetujuan yang jelas.
  • Hasil validasi mudah dibaca reviewer, bukan hanya log mentah.

Trade-off dibanding Integration Test Penuh

Kelebihan contract testing

  • Lebih cepat karena tidak selalu perlu menyiapkan environment penuh dan data kompleks.
  • Lebih fokus pada kompatibilitas kontrak, yaitu sumber regresi paling umum pada GraphQL.
  • Lebih mudah diskalakan untuk banyak consumer selama operasi mereka bisa dikumpulkan.
  • Lebih jelas untuk review karena perubahan schema dan dampaknya terlihat langsung.

Keterbatasan contract testing

  • Tidak menjamin resolver benar secara bisnis atau data benar secara semantik.
  • Tidak otomatis menangkap bug runtime seperti otorisasi salah, performa buruk, atau N+1 query.
  • Bergantung pada kelengkapan operasi client yang dikumpulkan.
  • Tidak selalu mendeteksi masalah kompatibilitas perilaku jika schema tetap valid tetapi makna data berubah.

Karena itu, contract testing sebaiknya melengkapi, bukan menggantikan sepenuhnya, integration test. Contract test menjaga kompatibilitas skema. Integration test memastikan perilaku aplikasi tetap benar.

Kapan Contract Testing Lebih Efektif untuk Backend Modern

Pendekatan ini paling efektif pada kondisi berikut:

  • Backend melayani banyak client seperti web, mobile, admin panel, atau partner API.
  • Rilis backend sering, sehingga risiko memutus client meningkat.
  • Schema berevolusi cepat dan tim ingin menjaga backward compatibility.
  • Arsitektur memakai persisted queries atau operasi terdokumentasi, sehingga consumer contract mudah dikumpulkan.
  • Tim terpisah antara backend dan frontend, sehingga validasi otomatis lebih andal daripada komunikasi manual.

Jika Anda hanya punya satu aplikasi kecil dengan satu tim dan perubahan schema jarang, contract testing mungkin terasa berat di awal. Tetapi ketika jumlah consumer bertambah atau deployment backend makin sering, manfaatnya cepat terlihat.

Kesalahan Umum dan Tips Debugging

Kesalahan umum

  • Hanya mengandalkan introspection diff tanpa memvalidasi query client nyata.
  • Menghapus field deprecated terlalu cepat karena mengira semua consumer sudah migrasi.
  • Tidak menyertakan fragment saat validasi, padahal banyak operasi bergantung padanya.
  • Menganggap penambahan enum value selalu aman padahal client bisa gagal pada logika aplikasi.
  • Memvalidasi schema lokal yang berbeda dari hasil build sebenarnya.

Tips debugging saat pipeline gagal

  • Baca error mulai dari operasi yang gagal divalidasi, bukan langsung dari diff schema.
  • Cari consumer mana yang masih memakai field deprecated atau enum value lama.
  • Bandingkan schema hasil CI dengan schema lokal untuk memastikan tidak ada perbedaan build.
  • Jika perubahan memang sengaja breaking, dokumentasikan migrasinya dan gunakan gate pengecualian yang eksplisit.
  • Untuk kasus nullability, periksa tidak hanya schema tetapi juga jaminan data dari resolver dan storage.

Catatan: pada GraphQL, perubahan yang tampak kecil di level SDL bisa berdampak besar pada code generation, caching client, dan validasi dokumen. Karena itu, validasi terhadap operasi consumer nyata hampir selalu lebih bernilai daripada sekadar membaca diff schema secara manual.

Penutup

Contract testing GraphQL adalah cara praktis untuk mencegah regresi skema API sebelum perubahan masuk ke branch utama. Dengan menggabungkan schema diff, validasi query client terhadap schema baru, dan gate di CI, tim bisa menahan breaking change pada field, nullability, enum, input type, maupun field deprecated yang masih dipakai.

Jika backend Anda melayani lebih dari satu consumer atau berubah cukup sering, ini biasanya memberi rasio manfaat terhadap biaya yang lebih baik daripada mengandalkan integration test penuh saja. Mulailah dari satu pipeline sederhana: simpan schema, kumpulkan operasi client, validasi di CI, lalu jadikan hasilnya syarat merge.