ข้ามไปยังเนื้อหา

Codegen and Testing

บทที่แล้วทิ้งช่องว่างไว้ข้อหนึ่ง คือ SDL แบบ schema-first ไม่ได้บังคับให้ resolver ตรงกับ schema ด้วยตัวเอง บทนี้ปิดช่องว่างนั้นจากสองทาง Code generation ให้ compiler บังคับ contract ตั้งแต่ก่อนคุณจะรันอะไร ส่วน testing พิสูจน์ว่า service ทำงานถูกต้องตอนรันจริง

GraphQL Code Generator อ่าน SDL แล้วสร้าง TypeScript type ที่สะท้อน schema แบบเป๊ะ ๆ ฝั่ง server จะได้ type Resolvers ที่ทำให้ทุก resolver ถูกตรวจกับ field ที่ implement อยู่ ส่วนฝั่ง client จะได้ type ของผลลัพธ์และ variable ของแต่ละ operation ที่คุณเขียน คุณเขียน config สั้น ๆ แล้วรันคำสั่ง จากนั้น type จะ regenerate ใหม่ทุกครั้งที่ schema เปลี่ยน

codegen.yml
schema: ./schema.graphql
generates:
./src/generated/graphql.ts:
plugins:
- typescript
- typescript-resolvers

ด้วย config นั้น generator จะแปลง SDL นี้:

type Track {
id: ID!
title: String!
durationSeconds: Int!
}
type Query {
track(id: ID!): Track
}

ให้เป็น TypeScript ที่คุณ import แล้วนำไปใช้กับ resolver map ของคุณ:

import type { Resolvers } from './generated/graphql';
// `Resolvers` is generated from the SDL. If `track` returned the wrong
// shape — or forgot a non-null field — this would be a compile error.
export const resolvers: Resolvers = {
Query: {
track: (_parent, args) => ({
id: args.id,
title: 'Walking the Graph',
durationSeconds: 214,
}),
},
};

ตอนนี้ schema-first ได้ safety แบบเดียวกับที่ code-first ได้มาฟรี ๆ SDL ยังเป็น contract ที่อ่านง่าย และ compiler จะไม่ยอม build ถ้า resolver drift ออกจาก SDL ลองเปลี่ยน durationSeconds เป็น String! ใน SDL ดู แล้ว resolver ด้านบนจะ compile ไม่ผ่านจนกว่าคุณจะแก้ นั่นคือจุดประสงค์ทั้งหมดของ codegen — ไม่ให้ drift หลุดขึ้น production

การตรวจ type พิสูจน์ว่า รูปร่าง ถูกต้อง ส่วน test พิสูจน์ว่า พฤติกรรม ถูกต้อง มีสองชั้นที่ควรค่าแก่การเขียน

  • Unit test สำหรับ resolver resolver ก็แค่ function ที่รับ (parent, args, context, info) เรียกตรง ๆ ด้วย input ปลอมแล้วตรวจค่าที่คืนกลับมาได้เลย test แบบนี้เร็วและตรึงตรรกะของ resolver ตัวเดียวไว้แน่น ไม่ต้องมี schema ไม่ต้องมี network
  • Integration test สำหรับ operation สร้าง executable schema แล้วรัน operation จริงด้วย function graphql จากนั้นตรวจ result.data กับ result.errors วิธีนี้ได้ทดสอบครบทั้ง parsing, validation, การเรียก resolver ที่ถูกตัว และเรื่อง nullability คือได้ contract แบบ end to end
flowchart TD
  SDL["SDL schema"]
  Codegen["GraphQL Code Generator"]
  Types["Generated Resolvers + operation types"]
  Unit["Unit tests: call resolver functions directly"]
  Integration["Integration tests: execute operations with graphql()"]
  Confidence["Trusted, shippable service"]
  SDL --> Codegen
  Codegen --> Types
  Types --> Unit
  Types --> Integration
  Unit --> Confidence
  Integration --> Confidence
codegen คุ้มกันรูปร่างตอน compile time ส่วน unit และ integration tests คุ้มกันพฤติกรรมตอน run time

ชั้น integration คือชั้นที่คนมักข้ามแล้วมาเสียใจทีหลัง เพราะเป็น test เดียวที่พิสูจน์ว่า query ที่ client จะส่งจริง ๆ คืนค่าตามที่ schema ระบุไว้ รวมถึงยืนยันว่า field แบบ non-null ไม่กลับมาเป็น null และ argument ส่งทะลุไปถึง resolver จริง

เดโมด้านล่างคือ integration test ฉบับย่อ สร้าง schema เล็ก ๆ รัน query จริงใส่ แล้วเช็ก assertion ไม่กี่ข้อ ซึ่งตรงกับที่ test framework อย่าง Vitest ทำอยู่เบื้องหลัง expect ตัวช่วย assert เล็ก ๆ จะพิมพ์บรรทัด pass หรือ fail ให้ทีละข้อ กด Run

JavaScript

อ่าน output เป็นรายงานผล test ได้เลย แต่ละบรรทัด PASS คือ expect หนึ่งข้อที่เป็นจริง assertion ของ id เป็นข้อที่น่าสนใจ เพราะพิสูจน์ว่า argument "t1" ไปถึง resolver จริงและกลับมาในข้อมูล ลองสลับค่าที่คาดไว้จาก 214 เป็น 999 แล้วรันใหม่ จะเห็นบรรทัด FAIL โผล่ขึ้นมา บรรทัดสีแดงนั้นแหละที่จับ regression ได้ก่อนหลุดขึ้น production

ข้อดีข้อแลกเปลี่ยน
type-safe resolver จาก schema — TypeScript type generate อัตโนมัติcodegen ต้องรัน ทุกครั้งที่ schema เปลี่ยน
client query ที่ type-safe — ไม่ต้อง type manuallysetup codegen config ครั้งแรกซับซ้อน
mock ใน test มาจาก schema — test ไม่ break เมื่อ schema เปลี่ยนgenerated type ที่ไม่ดูแล accumulate ใน codebase
integration test ที่ query จริงจับ bug ที่ unit test จับไม่ได้test database setup ซับซ้อนสำหรับ integration test

Codegen ที่ไม่ Integrate ใน CI/CD อาการ:

  • developer ลืม run codegen หลัง schema เปลี่ยน
  • TypeScript type ไม่ sync กับ schema จริง — false sense of type safety
  • run codegen ใน CI และ fail build ถ้า generated code ไม่ up-to-date

Test ที่ Mock ทุกอย่าง อาการ:

  • unit test ทุกตัว mock database, mock context, mock resolver
  • pass ทั้งหมดแต่ production ยัง fail เพราะ resolver ที่แท้จริงมี bug
  • เพิ่ม integration test ที่ใช้ schema และ resolver จริงกับ test database

💡 ตัวอย่างจากของจริง

Apollo Codegen:

  • generate TypeScript type จาก schema และ client query
  • ใช้แพร่หลายใน large TypeScript + GraphQL codebase

GraphQL Code Generator (The Guild):

  • plugin-based: generate resolver type, React hooks, Zod schema จาก GraphQL schema
  • ใช้ใน production ที่ Klarna, Airbnb, และบริษัทใหญ่อื่น ๆ
plugin typescript-resolvers ของ GraphQL Code Generator สร้างอะไรขึ้นมา?
code generation จับ error แบบใดที่ unit test จับไม่ได้?
integration test ออกกำลังให้ schema อย่างไร?
ในเดโม assertion ของ id พิสูจน์อะไรโดยเฉพาะ?