Codegen and Testing
บทที่แล้วทิ้งช่องว่างไว้ข้อหนึ่ง คือ SDL แบบ schema-first ไม่ได้บังคับให้ resolver ตรงกับ schema ด้วยตัวเอง บทนี้ปิดช่องว่างนั้นจากสองทาง Code generation ให้ compiler บังคับ contract ตั้งแต่ก่อนคุณจะรันอะไร ส่วน testing พิสูจน์ว่า service ทำงานถูกต้องตอนรันจริง
GraphQL Code Generator — types จาก schema
หัวข้อที่มีชื่อว่า “GraphQL Code Generator — types จาก schema”GraphQL Code Generator อ่าน SDL แล้วสร้าง TypeScript type ที่สะท้อน schema แบบเป๊ะ ๆ ฝั่ง server จะได้ type Resolvers ที่ทำให้ทุก resolver ถูกตรวจกับ field ที่ implement อยู่ ส่วนฝั่ง client จะได้ type ของผลลัพธ์และ variable ของแต่ละ operation ที่คุณเขียน คุณเขียน config สั้น ๆ แล้วรันคำสั่ง จากนั้น type จะ regenerate ใหม่ทุกครั้งที่ schema เปลี่ยน
schema: ./schema.graphqlgenerates: ./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
Testing — สองชั้น
หัวข้อที่มีชื่อว่า “Testing — สองชั้น”การตรวจ 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
ชั้น integration คือชั้นที่คนมักข้ามแล้วมาเสียใจทีหลัง เพราะเป็น test เดียวที่พิสูจน์ว่า query ที่ client จะส่งจริง ๆ คืนค่าตามที่ schema ระบุไว้ รวมถึงยืนยันว่า field แบบ non-null ไม่กลับมาเป็น null และ argument ส่งทะลุไปถึง resolver จริง
assertion สไตล์ test รันจริง
หัวข้อที่มีชื่อว่า “assertion สไตล์ test รันจริง”เดโมด้านล่างคือ integration test ฉบับย่อ สร้าง schema เล็ก ๆ รัน query จริงใส่ แล้วเช็ก assertion ไม่กี่ข้อ ซึ่งตรงกับที่ test framework อย่าง Vitest ทำอยู่เบื้องหลัง expect ตัวช่วย assert เล็ก ๆ จะพิมพ์บรรทัด pass หรือ fail ให้ทีละข้อ กด Run
อ่าน 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 manually | setup 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, และบริษัทใหญ่อื่น ๆ