

บทนำ: ยุทธศาสตร์การทดสอบสำหรับ tRPC ในโลก TypeScript
ในยุคที่การพัฒนาเว็บแอปพลิเคชันด้วย TypeScript กำลังเป็นที่นิยมสูงสุด tRPC (TypeScript Remote Procedure Call) ได้กลายเป็นเครื่องมือสำคัญที่ช่วยให้นักพัฒนาสามารถสร้าง API ที่ type-safe ได้อย่างง่ายดาย โดยไม่ต้องประกาศ schema ซ้ำซ้อนระหว่าง frontend และ backend อย่างไรก็ตาม ความท้าทายที่สำคัญคือการสร้างกลยุทธ์การทดสอบ (Testing Strategy) ที่มีประสิทธิภาพสำหรับ tRPC ซึ่งมีความแตกต่างจาก REST API หรือ GraphQL อย่างมีนัยสำคัญ
บทความนี้จะพาคุณดำดิ่งสู่โลกของการทดสอบ tRPC อย่างละเอียด ครอบคลุมตั้งแต่แนวคิดพื้นฐานไปจนถึงเทคนิคขั้นสูง พร้อมตัวอย่างโค้ดที่ใช้งานได้จริง ทั้งหมดนี้เขียนขึ้นสำหรับนักพัฒนา TypeScript ที่ต้องการยกระดับคุณภาพของแอปพลิเคชันของตนในปี 2026
1. ทำความเข้าใจธรรมชาติของ tRPC และความท้าทายในการทดสอบ
ก่อนที่เราจะลงลึกถึงกลยุทธ์การทดสอบ จำเป็นอย่างยิ่งที่ต้องเข้าใจว่า tRPC ทำงานอย่างไร และอะไรทำให้การทดสอบแตกต่างจาก API แบบดั้งเดิม
1.1 สถาปัตยกรรมของ tRPC
tRPC เป็นเฟรมเวิร์กที่ช่วยให้คุณสร้าง API แบบ RPC (Remote Procedure Call) โดยใช้ TypeScript เป็นภาษาหลัก จุดเด่นคือการ infer types จาก backend สู่ frontend โดยอัตโนมัติ ทำให้ไม่ต้องประกาศ types ซ้ำ ซึ่งช่วยลดความผิดพลาดและเพิ่มประสิทธิภาพในการพัฒนา
โครงสร้างพื้นฐานของ tRPC ประกอบด้วย:
- Router: ตัวกำหนดเส้นทางของ API endpoint
- Procedure: ฟังก์ชันที่ถูกเรียกใช้เมื่อมีการเรียก endpoint
- Context: ข้อมูลที่ถูกส่งผ่านทุก request เช่น authentication, database connection
- Middleware: ฟังก์ชันที่ทำงานก่อน/หลัง procedure (เช่น การตรวจสอบสิทธิ์)
1.2 ความท้าทายเฉพาะของ tRPC
การทดสอบ tRPC มีความท้าทายที่แตกต่างจากการทดสอบ REST API ดังนี้:
| ประเด็น | REST API | tRPC |
|---|---|---|
| การเรียก API | HTTP Request (GET, POST, PUT, DELETE) | ฟังก์ชัน TypeScript โดยตรง |
| การจัดการ Type | ต้องประกาศ types ซ้ำ (เช่น OpenAPI, Zod) | Types ถูก infer โดยอัตโนมัติ |
| การทดสอบ Integration | ต้องใช้ HTTP client (Supertest, Axios) | เรียกใช้ router โดยตรง |
| การ Mock | Mock HTTP layer | Mock function call |
| Error Handling | HTTP Status Codes | TRPCError (type-safe) |
ความท้าทายหลักคือการที่ tRPC เชื่อมต่อ frontend และ backend อย่างแนบแน่น ทำให้การแยกส่วนเพื่อทดสอบต้องใช้เทคนิคที่ซับซ้อนขึ้น
2. กลยุทธ์การทดสอบ tRPC แบบหลายชั้น (Layered Testing Strategy)
การทดสอบ tRPC ที่มีประสิทธิภาพต้องครอบคลุมหลายชั้น ตั้งแต่ unit test ไปจนถึง e2e test โดยแต่ละชั้นมีวัตถุประสงค์และเครื่องมือที่แตกต่างกัน
2.1 Unit Test สำหรับ Procedure แต่ละตัว
Unit Test เป็นชั้นพื้นฐานที่สุด โดยมุ่งเน้นการทดสอบ procedure แต่ละตัวแยกจากกัน โดย mocking dependencies ทั้งหมด เช่น database, external API, หรือ context
ตัวอย่างการเขียน Unit Test ด้วย Vitest:
// user.procedure.ts
import { z } from 'zod';
import { publicProcedure } from '../trpc';
import { db } from '../db';
export const getUserById = publicProcedure
.input(z.object({ id: z.string() }))
.query(async ({ input }) => {
const user = await db.user.findUnique({ where: { id: input.id } });
if (!user) {
throw new TRPCError({ code: 'NOT_FOUND', message: 'User not found' });
}
return user;
});
// user.procedure.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { getUserById } from './user.procedure';
// Mock database
vi.mock('../db', () => ({
db: {
user: {
findUnique: vi.fn(),
},
},
}));
describe('getUserById', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should return user when found', async () => {
const mockUser = { id: '1', name: 'John Doe', email: '[email protected]' };
(db.user.findUnique as any).mockResolvedValue(mockUser);
const result = await getUserById({
input: { id: '1' },
ctx: {}, // mock context
});
expect(result).toEqual(mockUser);
expect(db.user.findUnique).toHaveBeenCalledWith({ where: { id: '1' } });
});
it('should throw NOT_FOUND when user does not exist', async () => {
(db.user.findUnique as any).mockResolvedValue(null);
await expect(
getUserById({
input: { id: 'non-existent' },
ctx: {},
})
).rejects.toThrow(TRPCError);
});
});
ข้อดีของ Unit Test คือความเร็วและความแม่นยำในการระบุจุดบกพร่อง แต่ข้อเสียคือไม่สามารถตรวจจับปัญหาใน integration ได้
2.2 Integration Test สำหรับ Router
Integration Test จะทดสอบการทำงานร่วมกันของหลายๆ procedure ภายใน router เดียวกัน รวมถึง middleware และ context โดยอาจใช้ database จริงหรือ in-memory database แทนการ mock
ตัวอย่าง Integration Test ด้วย tRPC’s caller:
// app.router.ts
import { router } from '../trpc';
import { userRouter } from './user.router';
import { postRouter } from './post.router';
export const appRouter = router({
user: userRouter,
post: postRouter,
});
export type AppRouter = typeof appRouter;
// app.router.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { createTestContext } from '../test-utils';
import { appRouter } from './app.router';
describe('App Router Integration', () => {
let caller: ReturnType<typeof appRouter.createCaller>;
beforeAll(async () => {
const ctx = await createTestContext(); // สร้าง context สำหรับ test
caller = appRouter.createCaller(ctx);
});
afterAll(async () => {
// clean up test data
});
it('should create user and then create post', async () => {
// สร้าง user
const user = await caller.user.create({
name: 'Test User',
email: '[email protected]',
});
expect(user).toHaveProperty('id');
expect(user.name).toBe('Test User');
// สร้าง post โดยใช้ user id ที่เพิ่งสร้าง
const post = await caller.post.create({
title: 'Test Post',
content: 'This is a test post',
authorId: user.id,
});
expect(post).toHaveProperty('id');
expect(post.authorId).toBe(user.id);
});
it('should fail to create post with non-existent user', async () => {
await expect(
caller.post.create({
title: 'Invalid Post',
content: 'Content',
authorId: 'non-existent-id',
})
).rejects.toThrow(TRPCError);
});
});
การทดสอบแบบนี้ช่วยให้มั่นใจว่า router ทำงานได้ถูกต้องทั้งในแง่ของ business logic และ error handling
3. การจัดการ Context และ Middleware ในการทดสอบ
หนึ่งในความท้าทายที่พบบ่อยในการทดสอบ tRPC คือการจัดการ context และ middleware ซึ่งมักเกี่ยวข้องกับการ authentication, authorization, และการเชื่อมต่อฐานข้อมูล
3.1 การสร้าง Test Context
การสร้าง context สำหรับการทดสอบควรแยกออกจาก production context เพื่อให้สามารถควบคุม dependencies ได้อย่างเต็มที่
// test-utils.ts
import { inferAsyncReturnType } from '@trpc/server';
import { createContext } from '../context';
import { PrismaClient } from '@prisma/client';
import { mockDeep, DeepMockProxy } from 'vitest-mock-extended';
// สร้าง mock database
export const prismaMock = mockDeep<PrismaClient>();
export type TestContext = {
prisma: DeepMockProxy<PrismaClient>;
user?: { id: string; role: string };
};
export async function createTestContext(
overrides?: Partial<TestContext>
): Promise<TestContext> {
return {
prisma: prismaMock,
user: overrides?.user || { id: 'test-user-id', role: 'USER' },
};
}
// การใช้งานใน test
const ctx = await createTestContext({ user: { id: 'admin-1', role: 'ADMIN' } });
const caller = appRouter.createCaller(ctx);
3.2 การทดสอบ Middleware
Middleware ใน tRPC มักใช้สำหรับตรวจสอบสิทธิ์ (authorization) หรือการ validate input การทดสอบ middleware ควรแยกออกจาก procedure และทดสอบในบริบทต่างๆ
ตัวอย่างการทดสอบ middleware ที่ใช้ตรวจสอบ role:
// auth.middleware.ts
import { middleware, TRPCError } from '@trpc/server';
export const isAdmin = middleware(async ({ ctx, next }) => {
if (!ctx.user || ctx.user.role !== 'ADMIN') {
throw new TRPCError({
code: 'FORBIDDEN',
message: 'Only admins can perform this action',
});
}
return next({
ctx: {
user: ctx.user,
},
});
});
// auth.middleware.test.ts
import { describe, it, expect } from 'vitest';
import { isAdmin } from './auth.middleware';
describe('isAdmin middleware', () => {
it('should allow admin users', async () => {
const adminCtx = { user: { id: '1', role: 'ADMIN' } };
const next = vi.fn().mockResolvedValue({ data: 'success' });
const result = await isAdmin({
ctx: adminCtx,
next,
path: 'test',
type: 'query',
rawInput: {},
});
expect(next).toHaveBeenCalled();
expect(result.data).toBe('success');
});
it('should block non-admin users', async () => {
const userCtx = { user: { id: '2', role: 'USER' } };
const next = vi.fn();
await expect(
isAdmin({
ctx: userCtx,
next,
path: 'test',
type: 'query',
rawInput: {},
})
).rejects.toThrow(TRPCError);
});
it('should block unauthenticated users', async () => {
const noUserCtx = {};
const next = vi.fn();
await expect(
isAdmin({
ctx: noUserCtx as any,
next,
path: 'test',
type: 'query',
rawInput: {},
})
).rejects.toThrow(TRPCError);
});
});
4. การทดสอบ Error Handling และ Edge Cases
การจัดการข้อผิดพลาดเป็นส่วนสำคัญของแอปพลิเคชัน tRPC การทดสอบควรครอบคลุมทั้ง TRPCError ที่กำหนดเอง และข้อผิดพลาดที่ไม่คาดคิด
4.1 การทดสอบ TRPCError ที่กำหนดเอง
tRPC มี error codes มาตรฐาน เช่น NOT_FOUND, UNAUTHORIZED, FORBIDDEN, BAD_REQUEST, INTERNAL_SERVER_ERROR การทดสอบควรตรวจสอบทั้ง code และ message
// error.test.ts
import { describe, it, expect } from 'vitest';
import { TRPCError } from '@trpc/server';
describe('Error handling scenarios', () => {
it('should handle validation errors', async () => {
// สมมติว่า procedure มี input validation
try {
await caller.user.create({
name: '', // ชื่อว่าง
email: 'invalid-email',
});
} catch (error) {
expect(error).toBeInstanceOf(TRPCError);
expect(error.code).toBe('BAD_REQUEST');
expect(error.message).toContain('name');
expect(error.message).toContain('email');
}
});
it('should handle unauthorized access', async () => {
// สร้าง context ที่ไม่มี user
const unauthorizedCtx = await createTestContext({ user: undefined });
const caller = appRouter.createCaller(unauthorizedCtx);
await expect(
caller.admin.deleteUser({ id: 'some-id' })
).rejects.toMatchObject({
code: 'UNAUTHORIZED',
message: 'You must be logged in',
});
});
it('should handle database connection errors gracefully', async () => {
// Mock database ให้ throw error
prismaMock.user.findUnique.mockRejectedValue(
new Error('Database connection lost')
);
await expect(
caller.user.getById({ id: '1' })
).rejects.toMatchObject({
code: 'INTERNAL_SERVER_ERROR',
message: 'An unexpected error occurred',
});
});
});
4.2 การทดสอบ Edge Cases
Edge cases ที่ควรทดสอบสำหรับ tRPC ได้แก่:
- Input ที่มีค่าขอบเขต (boundary values) เช่น string ยาวมาก, ตัวเลขติดลบ
- การเรียก procedure ที่ไม่มีอยู่ (invalid path)
- การส่งข้อมูลซ้ำ (idempotency) โดยเฉพาะ mutation
- การทำงานพร้อมกัน (concurrency) เช่น การ update record เดียวกันจากหลาย request
- การ timeout หรือ network error (สำหรับการทดสอบ client-side)
5. การทดสอบ Client-side Integration
การทดสอบฝั่ง client ที่เรียกใช้ tRPC มีความสำคัญไม่แพ้ฝั่ง server โดยเฉพาะอย่างยิ่งเมื่อ tRPC มี type safety ที่แข็งแกร่ง
5.1 การ Mock tRPC Client
ในการทดสอบ frontend component ที่ใช้ tRPC hooks เราสามารถ mock tRPC client ได้โดยใช้ utilities ที่ tRPC จัดเตรียมไว้ให้
// components/UserProfile.tsx
import { trpc } from '../utils/trpc';
export function UserProfile({ userId }: { userId: string }) {
const { data: user, isLoading, error } = trpc.user.getById.useQuery({ id: userId });
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
</div>
);
}
// components/UserProfile.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { UserProfile } from './UserProfile';
import { createTRPCMock } from '../test-utils';
describe('UserProfile Component', () => {
it('should display user data when loaded', async () => {
const mockUser = {
id: '1',
name: 'John Doe',
email: '[email protected]',
};
// สร้าง mock tRPC client
const trpc = createTRPCMock({
user: {
getById: {
query: vi.fn().mockResolvedValue(mockUser),
},
},
});
render(<UserProfile userId="1" />);
// ตรวจสอบว่าแสดง loading state ก่อน
expect(screen.getByText('Loading...')).toBeDefined();
// รอให้ data โหลดเสร็จ
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeDefined();
expect(screen.getByText('Email: [email protected]')).toBeDefined();
});
});
it('should display error state', async () => {
const trpc = createTRPCMock({
user: {
getById: {
query: vi.fn().mockRejectedValue(new Error('Network error')),
},
},
});
render(<UserProfile userId="1" />);
await waitFor(() => {
expect(screen.getByText(/Error:/)).toBeDefined();
});
});
});
5.2 การทดสอบ Mutation ด้วย Optimistic Updates
tRPC รองรับ optimistic updates ซึ่งช่วยให้ UI ตอบสนองได้ทันที การทดสอบกรณีนี้ต้องตรวจสอบทั้ง optimistic state และ final state หลังจาก server ตอบกลับ
// components/PostEditor.tsx
export function PostEditor({ postId }: { postId: string }) {
const utils = trpc.useContext();
const updatePost = trpc.post.update.useMutation({
onMutate: async (newPost) => {
// Optimistic update
await utils.post.getById.cancel({ id: postId });
const previousData = utils.post.getById.getData({ id: postId });
utils.post.getById.setData({ id: postId }, (old) => ({
...old!,
...newPost,
}));
return { previousData };
},
onError: (err, newPost, context) => {
// Rollback
utils.post.getById.setData({ id: postId }, context?.previousData);
},
onSettled: () => {
// Refetch
utils.post.getById.invalidate({ id: postId });
},
});
// ... component logic
}
// components/PostEditor.test.tsx
it('should perform optimistic update and rollback on error', async () => {
const mockPost = { id: '1', title: 'Original Title', content: 'Content' };
const newTitle = 'Updated Title';
// Mock mutation ให้ล้มเหลว
const trpc = createTRPCMock({
post: {
update: {
mutation: vi.fn().mockRejectedValue(new Error('Save failed')),
},
getById: {
query: vi.fn().mockResolvedValue(mockPost),
// Mock cache manipulation functions
setData: vi.fn(),
getData: vi.fn().mockReturnValue(mockPost),
cancel: vi.fn(),
invalidate: vi.fn(),
},
},
});
render(<PostEditor postId="1" />);
// จำลองการแก้ไข title
await userEvent.type(screen.getByLabelText('Title'), newTitle);
await userEvent.click(screen.getByText('Save'));
// ตรวจสอบ optimistic update
expect(trpc.post.update.mutation).toHaveBeenCalled();
// ตรวจสอบ rollback เมื่อเกิด error
await waitFor(() => {
expect(screen.getByDisplayValue('Original Title')).toBeDefined();
});
});
6. การเปรียบเทียบเครื่องมือและแนวทางปฏิบัติที่ดีที่สุด
ในการเลือกเครื่องมือสำหรับทดสอบ tRPC มีหลายตัวเลือก แต่ละตัวมีข้อดีข้อเสียแตกต่างกันไป
6.1 เปรียบเทียบ Testing Frameworks
| เครื่องมือ | จุดเด่น | จุดด้อย | เหมาะกับ |
|---|---|---|---|
| Vitest | เร็ว, รองรับ ESM, เข้ากับ Vite/Next.js | ยังใหม่, ecosystem น้อยกว่า Jest | โปรเจกต์ที่ใช้ Vite หรือ modern stack |
| Jest | 成熟, มี community ขนาดใหญ่, plugins มาก | ช้า, การตั้งค่าซับซ้อน, ESM support จำกัด | โปรเจกต์ legacy หรือต้องการ stability |
| Playwright | E2E testing ที่แข็งแกร่ง, รองรับหลาย browser | ช้า, ใช้ทรัพยากรมาก | การทดสอบ end-to-end และ visual testing |
| Supertest (สำหรับ tRPC) | ทดสอบ HTTP layer จริง | ต้องตั้งค่า server, ช้ากว่า direct call | การทดสอบ integration ที่ต้องใช้ HTTP |
6.2 Best Practices สำหรับการทดสอบ tRPC
จากประสบการณ์ในโปรเจกต์จริง เราได้รวบรวมแนวทางปฏิบัติที่ดีที่สุดดังนี้:
- แยก business logic ออกจาก procedure – เขียน logic ใน service layer ที่แยกออกมา เพื่อให้สามารถทดสอบ unit test ได้โดยไม่ต้องพึ่ง tRPC
- ใช้ dependency injection – ส่ง dependencies (เช่น database client, external API) ผ่าน context แทนการ import โดยตรง
- ทดสอบ middleware แยกต่างหาก – middleware ควรถูกทดสอบเป็นหน่วยอิสระ ก่อนนำไปใช้ใน router
- ใช้ test database แยก – สำหรับ integration test ควรใช้ database ที่แยกจาก production เช่น SQLite in-memory หรือ Docker container
- สร้าง test utilities – สร้าง helper functions สำหรับการสร้าง context, mock data, และ caller เพื่อลด code ซ้ำซ้อน
- ทดสอบทั้ง success และ failure paths – อย่าลืมทดสอบกรณี error, edge cases, และ unexpected inputs
- ใช้ type safety ให้เป็นประโยชน์ – tRPC ให้ type safety ที่แข็งแกร่ง ใช้ TypeScript compiler เป็นเครื่องมือทดสอบเบื้องต้น
- จัดลำดับความสำคัญของ test layers – 80% unit test, 15% integration test, 5% e2e test (ตาม Test Pyramid)
7. กรณีศึกษาจากโลกจริง (Real-World Use Cases)
เพื่อให้เห็นภาพชัดเจนยิ่งขึ้น เราจะนำเสนอกรณีศึกษาจากโปรเจกต์จริงที่ใช้ tRPC และกลยุทธ์การทดสอบที่เราได้กล่าวถึง
7.1 กรณีศึกษา: แพลตฟอร์ม E-Commerce ขนาดกลาง
ความท้าทาย: ทีมพัฒนาประสบปัญหาการทดสอบที่ใช้เวลานาน เพราะต้องทดสอบทั้ง frontend และ backend แยกจากกัน และมักพบข้อผิดพลาดที่เกิดจาก type mismatch ระหว่างสองฝั่ง
แนวทางแก้ไข:
- ใช้ tRPC เพื่อให้ type safety ระหว่าง frontend-backend
- สร้าง test suite ที่ครอบคลุม 3 ระดับ:
- Unit test สำหรับ service layer (business logic)
- Integration test สำหรับ tRPC router (รวม middleware และ context)
- Component test สำหรับ React components ที่ใช้ tRPC hooks
- ใช้ Vitest + Testing Library สำหรับ frontend
- ใช้ in-memory SQLite สำหรับ integration test เพื่อความเร็ว
ผลลัพธ์: ลดเวลาในการ debug ลง 60%, เพิ่ม code coverage จาก 45% เป็น 85%, และลดจำนวนบั๊กที่พบใน production ลง 70%
7.2 กรณีศึกษา: ระบบจัดการเอกสารภายในองค์กร
ความท้าทาย: ระบบมี workflow ที่ซับซ้อน เกี่ยวข้องกับหลายขั้นตอนและการอนุมัติจากผู้ใช้หลายระดับ การทดสอบ manual ใช้เวลานานและเกิดข้อผิดพลาดบ่อย
แนวทางแก้ไข:
- ใช้ tRPC mutation แบบมี middleware สำหรับ authorization
- เขียน integration test ที่จำลอง workflow ทั้งหมด ตั้งแต่สร้างเอกสาร ไปจนถึงอนุมัติ
- ใช้ Playwright สำหรับ e2e test เพื่อทดสอบ UI flow
- สร้าง test data factory สำหรับสร้างข้อมูลทดสอบที่ซับซ้อน
ผลลัพธ์: การทดสอบ regression ที่เคยใช้เวลา 2 วัน ลดลงเหลือ 30 นาที, สามารถ deploy ได้บ่อยขึ้น (จากเดือนละครั้งเป็นสัปดาห์ละครั้ง)
8. เครื่องมือและเทคนิคขั้นสูง
สำหรับทีมที่ต้องการยกระดับการทดสอบ tRPC ไปอีกขั้น มีเครื่องมือและเทคนิคที่น่าสนใจดังนี้
8.1 การใช้ tRPC Server-side Caller
หนึ่งในฟีเจอร์ที่ทรงพลังของ tRPC คือความสามารถในการเรียก procedure โดยตรงจากฝั่ง server โดยไม่ต้องผ่าน HTTP ซึ่งช่วยให้การทดสอบ integration ทำได้ง่ายและเร็วขึ้น
// server-side caller test
import { createCallerFactory } from '@trpc/server';
import { appRouter } from './router';
// สร้าง caller factory
const createCaller = createCallerFactory(appRouter);
// ใช้ใน test
async function setupTest() {
const ctx = await createTestContext();
const caller = createCaller(ctx);
return caller;
}
// ตัวอย่าง test
it('should handle complex queries', async () => {
const caller = await setupTest();
// เรียก procedure โดยตรง
const result = await caller.product.search({
query: 'laptop',
filters: { minPrice: 10000, maxPrice: 50000 },
sort: 'price_asc',
page: 1,
});
expect(result.items).toHaveLength(10);
expect(result.totalPages).toBeGreaterThan(0);
});
8.2 การใช้ Snapshot Testing สำหรับ tRPC Response
Snapshot testing สามารถใช้เพื่อตรวจสอบว่า response จาก tRPC procedure ไม่เปลี่ยนแปลงโดยไม่ตั้งใจ
// snapshot test
it('should match snapshot for getProducts', async () => {
const caller = await setupTest();
const result = await caller.product.getProducts({
category: 'electronics',
limit: 5,
});
expect(result).toMatchSnapshot();
});
// เมื่อมีการเปลี่ยนแปลง intentionally ให้อัพเดท snapshot
// npx vitest --update
8.3 การใช้ Faker.js สำหรับสร้าง Test Data
การสร้างข้อมูลทดสอบที่สมจริงช่วยให้การทดสอบครอบคลุมกรณีต่างๆ ได้ดีขึ้น
// test-data-factory.ts
import { faker } from '@faker-js/faker';
export function createUser(overrides?: Partial<User>): User {
return {
id: faker.string.uuid(),
name: faker.person.fullName(),
email: faker.internet.email(),
role: faker.helpers.arrayElement(['USER', 'ADMIN', 'MODERATOR']),
createdAt: faker.date.past(),
...overrides,
};
}
export function createProduct(overrides?: Partial<Product>): Product {
return {
id: faker.string.uuid(),
name: faker.commerce.productName(),
price: parseFloat(faker.commerce.price()),
category: faker.commerce.department(),
stock: faker.number.int({ min: 0, max: 1000 }),
...overrides,
};
}
// ใช้ใน test
it('should create order with multiple items', async () => {
const user = createUser();
const products = Array.from({ length: 3 }, () => createProduct());
const order = await caller.order.create({
userId: user.id,
items: products.map(p => ({
productId: p.id,
quantity: faker.number.int({ min: 1, max: 5 }),
})),
});
expect(order.items).toHaveLength(3);
expect(order.total).toBeGreaterThan(0);
});
9. การตั้งค่า CI/CD สำหรับ tRPC Testing
การทดสอบอัตโนมัติใน CI/CD pipeline เป็นสิ่งจำเป็นสำหรับทีมที่ต้องการ quality assurance อย่างต่อเนื่อง
9.1 ตัวอย่าง GitHub Actions Workflow
# .github/workflows/test.yml
name: tRPC Testing Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Generate Prisma client
run: npx prisma generate
- name: Run database migrations
run: npx prisma migrate deploy
env:
DATABASE_URL: postgresql://test:test@localhost:5432/testdb
- name: Run linting
run: npm run lint
- name: Run TypeScript type check
run: npx tsc --noEmit
- name: Run unit and integration tests
run: npm run test:ci
env:
DATABASE_URL: postgresql://test:test@localhost:5432/testdb
- name: Run e2e tests
run: npm run test:e2e
env:
DATABASE_URL: postgresql://test:test@localhost:5432/testdb
- name: Upload coverage reports
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
flags: unittests
name: codecov-umbrella
9.2 การตั้งค่า Test Runner สำหรับ CI
ควรปรับแต่งการตั้งค่า Vitest หรือ Jest ให้เหมาะสมกับสภาพแวดล้อม CI:
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
setupFiles: ['./src/test-setup.ts'],
include: ['src/**/*.{test,spec}.{ts,tsx}'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'lcov'],
include: ['src/**/*.ts'],
exclude: [
'src/**/*.test.ts',
'src/**/*.spec.ts',
'src/test-utils/**',
],
thresholds: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
testTimeout: 30000,
hookTimeout: 30000,
reporters: process.env.CI
? ['default', 'junit']
: ['default'],
outputFile: process.env.CI
? { junit: './test-results/junit.xml' }
: undefined,
},
});
10. ข้อผิดพลาดที่พบบ่อยและวิธีแก้ไข
จากการทำงานกับ tRPC testing ในหลายโปรเจกต์ เราพบข้อผิดพลาดที่พบบ่อยดังนี้:
| ปัญหา | สาเหตุ | วิธีแก้ไข |
|---|---|---|
| TypeError: Cannot read properties of undefined (reading ‘query’) | ไม่ได้สร้าง caller อย่างถูกต้อง หรือ context ขาด dependencies ที่จำเป็น | ตรวจสอบว่า createCaller ถูกเรียกด้วย context ที่สมบูรณ์ |
| Test ใช้เวลานานเกินไป | ใช้ database จริง หรือมีการทำ network call | เปลี่ยนเป็น in-memory database, mock external APIs |
| Test flaky (บางครั้งผ่าน บางครั้งไม่ผ่าน) | การทำงาน async
Log In
|