Test adalah kode yang memeriksa kode-mu secara otomatis: ia menjalankan sesuatu, lalu membandingkan hasilnya dengan yang seharusnya. Untuk API GraphQL, yang diperiksa ada empat lapis: resolver, operation yang dijalankan terhadap schema, operation milik tiap aplikasi client, dan schema itu sendiri. Bayangin Toko Buku sebelum buka. Kasir menjalankan daftar belanja percobaan: scan Laskar Pelangi harus muncul 89.000, restock Bumi Manusia harus menambah stok, dan kartu member lama dari pelanggan tetap harus masih diterima mesin kasir yang baru.
🧪 Tebakan cepat: Resolver
bookmelemparGraphQLError("Book not found"). Di dalam test,const result = await run("{ book(id: 99) { title } }"). Apakah baris itu melempar error sehingga test-nya gagal sendiri? Jawabannya ada di bagian "Menjalankan operation terhadap schema".
Resolver adalah fungsi biasa
Resolver cuma fungsi (parent, args, context, info). Jadi bisa dipanggil langsung di test, dengan data palsu dan context buatan. Contoh dengan Vitest (versi 5.0.3; Jest sama persis untuk bagian ini):
import { describe, expect, test } from "vitest";
import { createResolvers } from "./resolvers.js";
const freshDb = () => ({
books: [{ id: 2, title: "Bumi Manusia", authorId: 2, price: 125000, stock: 0 }],
authors: [{ id: 2, name: "Pramoedya Ananta Toer" }],
});
describe("resolver sebagai fungsi biasa", () => {
test('Query.book(id: "2") memberi Bumi Manusia', () => {
const resolvers = createResolvers(freshDb());
expect(resolvers.Query.book(null, { id: "2" }, { user: null }).title).toBe("Bumi Manusia");
});
test("Mutation.updateStock menyimpan stok baru", () => {
const db = freshDb();
createResolvers(db).Mutation.updateStock(null, { id: "2", stock: 7 }, { user: null });
expect(db.books[0].stock).toBe(7);
});
});
freshDb()membuat data baru untuk tiap test, supaya mutation di satu test gak mengubah data test lain.iddikirim sebagai string"2", karena begitulah GraphQL memberikan typeIDke resolver.- Cepat dan tepat sasaran, tapi ada yang gak teruji: validasi query, konversi argument, dan apakah resolver-nya memang terpasang di schema.
Menjalankan operation terhadap schema
Test yang paling berharga mengirim operation sungguhan: parsing, validasi, resolver, dan bentuk response diuji bersama. Ini yang disebut integration test, dan untuk GraphQL ini pendekatan terpenting. Dengan graphql-js 17.0.2 dan @graphql-tools/schema 10.1.3:
import { graphql } from "graphql";
import { makeExecutableSchema } from "@graphql-tools/schema";
const run = (source, variableValues) => {
const schema = makeExecutableSchema({ typeDefs, resolvers: createResolvers(freshDb()) });
return graphql({ schema, source, variableValues, contextValue: { user: null } }); // context untuk test
};
test("Detail: judul dan stok", async () => {
const result = await run("query Detail($id: ID!) { book(id: $id) { title stock } }", { id: "2" });
expect(result).toEqual({ data: { book: { title: "Bumi Manusia", stock: 0 } } });
});
test("book(id: 99): data null dan NOT_FOUND", async () => {
const result = await run("{ book(id: 99) { title } }");
expect(result.data).toEqual({ book: null });
expect(result.errors[0].extensions.code).toBe("NOT_FOUND");
});
Response untuk book(id: 99):
{"errors":[{"message":"Book not found","locations":[{"line":1,"column":3}],"path":["book"],"extensions":{"code":"NOT_FOUND"}}],"data":{"book":null}}
graphql() gak pernah melempar karena error resolver. Error-nya masuk ke errors, field-nya jadi null, dan response tetap dikembalikan. Itu jawaban tebakannya: test gak gagal sendiri, jadi kamu wajib memeriksa errors. Test yang cuma memeriksa data bisa lulus padahal ada error.
Kalau server-mu Apollo Server 5.5.1, server.executeOperation({ query, variables }, { contextValue }) menjalankan operation lewat pipeline-nya, plugin ikut jalan. Hasilnya { body: { kind: "single", singleResult: { data } } }, jadi baca response.body.singleResult.
Apa saja yang perlu dites saat schema baru dibuat: schema-nya valid (buildSchema melempar Unknown type "Boook". untuk type yang salah ketik), query mengembalikan data yang benar, mutation benar-benar mengubah data, penanganan error (input salah, belum login, gak boleh), dan kasus pinggir seperti null dan list kosong (Ahmad Tohari belum punya buku, jadi books: []).
Mock: data palsu dari schema
addMocksToSchema dari @graphql-tools/mock 9.1.16 membuat schema yang menjawab query apa pun dengan data palsu, tanpa resolver. Berguna untuk frontend yang mulai lebih dulu, atau untuk test di client:
import { addMocksToSchema } from "@graphql-tools/mock";
const mocked = addMocksToSchema({ schema: makeExecutableSchema({ typeDefs }) });
await graphql({ schema: mocked, source: "{ books { title price status } }" });
// {"data":{"books":[{"title":"Hello World","price":20,"status":"IN_STOCK"},{"title":"Hello World","price":-100,"status":"IN_STOCK"}]}}
String jadi "Hello World", Int dan enum acak, list berisi 2 item. Ganti dengan mocks: { Int: () => 89000 }, atau pakai preserveResolvers: true supaya resolver yang sudah ada tetap dipakai. Ingat, mock menguji bentuk, bukan isi.
Snapshot atau assertion eksplisit
expect(result).toMatchSnapshot() menyimpan hasil pertama ke file __snapshots__, lalu membandingkan tiap run berikutnya; kalau beda, test gagal dengan Snapshot ... mismatched, dan vitest -u menimpanya. Gampang ditulis, tapi snapshot cuma tahu "beda", bukan "salah". Saat tim terbiasa menekan update tanpa membaca diff-nya, snapshot gak melindungi apa-apa. Pakai assertion eksplisit (toEqual, toBe) untuk perilaku, dan snapshot untuk teks yang memang ingin ditinjau saat berubah, misalnya printSchema(schema).
Cek perubahan schema di CI
Pelajaran "Schema Design Patterns" menunjukkan perubahan yang memecah client lama. Di CI, graphql-inspector (CLI 7.0.0, core 8.0.0) membandingkan schema di branch main dengan schema di pull request: npx graphql-inspector diff git:origin/main:schema.graphql schema.graphql. Tiap perubahan diberi tingkat:
| Perubahan | Tingkat | Pesan |
|---|---|---|
price diganti priceRupiah |
BREAKING | Field 'price' was removed from object type 'Book' |
| NON_BREAKING | Field 'priceRupiah' was added to object type 'Book' |
|
status jadi String |
BREAKING | Field 'Book.status' changed type from 'StockStatus!' to 'String!' |
| BREAKING | Type for argument 'status' on field 'Query.books' changed from 'StockStatus' to 'String' |
|
nilai enum HABIS ditambah |
DANGEROUS | Enum value 'HABIS' was added to enum 'StockStatus' |
price diberi @deprecated |
NON_BREAKING | Field 'Book.price' is deprecated |
BREAKING memecah client yang ada, DANGEROUS bisa memecah client yang gak siap (misalnya switch tanpa cabang untuk HABIS), NON_BREAKING aman. Kalau ada BREAKING, perintah itu keluar dengan kode 1 (Detected 2 breaking changes), jadi merge-nya diblok. Apollo GraphOS punya rover graph check dan rover subgraph check, yang memeriksa perubahan terhadap operation yang benar-benar dipakai client.
Kontrak untuk client
Test kontrak memastikan operation tiap aplikasi client masih valid terhadap schema baru. Operation diambil dari repo tiap aplikasi (atau dari manifest trusted documents), lalu divalidasi:
import { buildSchema, parse, validate } from "graphql";
test("web toko: Katalog masih valid", () => {
const errors = validate(buildSchema(typeDefs), parse("query Katalog { books { title price status } }"));
expect(errors.map((e) => e.message)).toEqual([]);
});
Setelah price diganti nama, test ini gagal dengan pesan asli graphql-js: Cannot query field "price" on type "Book". Setelah status jadi String, kontrak dashboard admin gagal: String cannot represent a non string value: SOLD_OUT. CLI-nya: graphql-inspector validate "./src/**/*.graphql" schema.graphql.
⚠️ Jebakan yang sering ketemu
- Cuma memeriksa
data.graphql()gak melempar; error-nya diam dierrors. - Cuma unit test resolver. Resolver yang benar tapi lupa dipasang di map resolver gak akan ketahuan.
- Snapshot yang di-update tanpa dibaca. Test-nya hijau lagi, bug-nya tetap ada.
- Data yang bocor antar test. Mutation di satu test mengubah stok untuk test berikutnya. Buat data segar per test (
beforeEachataufreshDb()).
TL;DR: Resolver adalah fungsi biasa, jadi bisa dites langsung dengan data dan context palsu, tapi itu gak menguji schema. Test terpenting menjalankan operation sungguhan terhadap schema (graphql({ schema, source, variableValues, contextValue }) atau server.executeOperation di Apollo Server) dan memeriksa data dan errors, karena graphql() gak melempar. Uji schema valid, data query, efek mutation, error (input salah, belum login), dan kasus pinggir. addMocksToSchema memberi data palsu dari schema saja. Assertion eksplisit untuk perilaku, snapshot cuma untuk teks yang ingin ditinjau. Di CI, graphql-inspector diff menandai perubahan BREAKING, DANGEROUS, atau NON_BREAKING dan memblok merge kalau ada yang BREAKING, sedangkan test kontrak memvalidasi operation tiap aplikasi client terhadap schema baru.