TypeScript tRPC Testing Strategy QA — คู่มือฉบับสมบูรณ์ 2026 | SiamCafe Blog

TypeScript tRPC Testing Strategy QA — คู่มือฉบับสมบูรณ์ 2026 | SiamCafe Blog

บทนำ: ยุทธศาสตร์การทดสอบสำหรับ 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

จากประสบการณ์ในโปรเจกต์จริง เราได้รวบรวมแนวทางปฏิบัติที่ดีที่สุดดังนี้:

  1. แยก business logic ออกจาก procedure – เขียน logic ใน service layer ที่แยกออกมา เพื่อให้สามารถทดสอบ unit test ได้โดยไม่ต้องพึ่ง tRPC
  2. ใช้ dependency injection – ส่ง dependencies (เช่น database client, external API) ผ่าน context แทนการ import โดยตรง
  3. ทดสอบ middleware แยกต่างหาก – middleware ควรถูกทดสอบเป็นหน่วยอิสระ ก่อนนำไปใช้ใน router
  4. ใช้ test database แยก – สำหรับ integration test ควรใช้ database ที่แยกจาก production เช่น SQLite in-memory หรือ Docker container
  5. สร้าง test utilities – สร้าง helper functions สำหรับการสร้าง context, mock data, และ caller เพื่อลด code ซ้ำซ้อน
  6. ทดสอบทั้ง success และ failure paths – อย่าลืมทดสอบกรณี error, edge cases, และ unexpected inputs
  7. ใช้ type safety ให้เป็นประโยชน์ – tRPC ให้ type safety ที่แข็งแกร่ง ใช้ TypeScript compiler เป็นเครื่องมือทดสอบเบื้องต้น
  8. จัดลำดับความสำคัญของ 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

จัดส่งรวดเร็วส่งด่วนทั่วประเทศ
รับประกันสินค้าเคลมง่าย มีใบรับประกัน
ผ่อนชำระได้บัตรเครดิต 0% สูงสุด 10 เดือน
สะสมแต้ม รับส่วนลดส่วนลดและคะแนนสะสม

© 2026 SiamLancard — จำหน่ายการ์ดแลน อุปกรณ์ Server และเครื่องพิมพ์ใบเสร็จ

SiamLancard
Logo
Shopping cart