mcpField
M
mocking-assistant

mocking-assistant

github.com/patricio0312rev测试与质量63244 次安装41 周前更新
代码仓库

关于 mocking-assistant

面向编写测试的开发者,提供 API、服务和函数模拟的实践指导。内容包括 MSW、Vitest、固定测试数据、GraphQL 及时间模拟。

主要功能

  1. 01使用 MSW 模拟 HTTP API
  2. 02集中管理测试固定数据
  3. 03按测试覆盖模拟处理器
  4. 04模拟服务与函数
  5. 05控制时间并模拟 GraphQL

适用场景

  1. 01隔离测试中的 API 请求
  2. 02为特定测试覆盖默认处理器
  3. 03验证错误场景与加载状态
  4. 04模拟 GraphQL 查询和时间

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/patricio0312rev/skills --skill mocking-assistant

安装后在 Claude Code 中输入/mocking-assistant确认已加载。

详细介绍

模拟助手

为测试中的 API 和服务创建可靠的模拟。

MSW API 模拟

// mocks/handlers.ts
import { http, HttpResponse } from "msw";

export const handlers = [
  // GET endpoint
  http.get("/api/users/:id", ({ params }) => {
    const { id } = params;

    return HttpResponse.json({
      id,
      name: "John Doe",
      email: "[email protected]",
    });
  }),

  // POST endpoint
  http.post("/api/users", async ({ request }) => {
    const body = await request.json();

    return HttpResponse.json(
      {
        id: Math.random().toString(),
        ...body,
        createdAt: new Date().toISOString(),
      },
      { status: 201 }
    );
  }),

  // Error response
  http.get("/api/products/:id", ({ params }) => {
    const { id } = params;

    if (id === "404") {
      return HttpResponse.json({ error: "Product not found" }, { status: 404 });
    }

    return HttpResponse.json({
      id,
      name: "MacBook Pro",
      price: 2499.99,
    });
  }),

  // Delayed response
  http.get("/api/slow-endpoint", async () => {
    await delay(2000);
    return HttpResponse.json({ data: "Slow response" });
  }),
];

MSW 设置

// mocks/server.ts
import { setupServer } from "msw/node";
import { handlers } from "./handlers";

export const server = setupServer(...handlers);

// tests/setup.ts
import { beforeAll, afterEach, afterAll } from "vitest";
import { server } from "../mocks/server";

beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

测试数据约定

// mocks/fixtures/users.ts
export const userFixtures = {
  admin: {
    id: "1",
    email: "[email protected]",
    name: "Admin User",
    role: "ADMIN",
  },
  customer: {
    id: "2",
    email: "[email protected]",
    name: "Customer User",
    role: "USER",
  },
  guest: {
    id: "3",
    email: "[email protected]",
    name: "Guest User",
    role: "GUEST",
  },
};

// mocks/fixtures/products.ts
export const productFixtures = {
  laptop: {
    id: "100",
    name: "MacBook Pro",
    price: 2499.99,
    stock: 10,
    category: "Electronics",
  },
  phone: {
    id: "101",
    name: "iPhone 15",
    price: 999.99,
    stock: 50,
    category: "Electronics",
  },
  outOfStock: {
    id: "102",
    name: "Sold Out Item",
    price: 499.99,
    stock: 0,
    category: "Electronics",
  },
};

// Usage in handlers
http.get("/api/users/:id", ({ params }) => {
  const user = Object.values(userFixtures).find((u) => u.id === params.id);

  if (!user) {
    return HttpResponse.json({ error: "User not found" }, { status: 404 });
  }

  return HttpResponse.json(user);
});

针对特定测试的模拟

// tests/components/UserProfile.test.tsx
import { server } from "../mocks/server";
import { http, HttpResponse } from "msw";

test("should display user profile", async () => {
  // Override handler for this test
  server.use(
    http.get("/api/users/123", () => {
      return HttpResponse.json({
        id: "123",
        name: "Test User",
        email: "[email protected]",
      });
    })
  );

  render(<UserProfile userId="123" />);

  await waitFor(() => {
    expect(screen.getByText("Test User")).toBeInTheDocument();
  });
});

test("should handle API error", async () => {
  // Mock error response
  server.use(
    http.get("/api/users/123", () => {
      return HttpResponse.json({ error: "Server error" }, { status: 500 });
    })
  );

  render(<UserProfile userId="123" />);

  await waitFor(() => {
    expect(screen.getByText("Failed to load user")).toBeInTheDocument();
  });
});

服务模拟

// src/services/paymentService.ts
export interface PaymentService {
  processPayment(amount: number, cardToken: string): Promise<PaymentResult>;
  refund(transactionId: string): Promise<void>;
}

// mocks/services/mockPaymentService.ts
export class MockPaymentService implements PaymentService {
  async processPayment(
    amount: number,
    cardToken: string
  ): Promise<PaymentResult> {
    // Simulate successful payment
    if (cardToken.startsWith("tok_success")) {
      return {
        transactionId: "txn_" + Math.random().toString(36),
        status: "success",
        amount,
      };
    }

    // Simulate failed payment
    if (cardToken.startsWith("tok_fail")) {
      throw new Error("Payment failed");
    }

    // Simulate slow payment
    await new Promise((resolve) => setTimeout(resolve, 2000));
    return {
      transactionId: "txn_" + Math.random().toString(36),
      status: "success",
      amount,
    };
  }

  async refund(transactionId: string): Promise<void> {
    // Mock refund
    console.log(`Refunding transaction: ${transactionId}`);
  }
}

// tests/checkout.test.ts
const mockPaymentService = new MockPaymentService();

test("should process payment successfully", async () => {
  const result = await mockPaymentService.processPayment(
    100,
    "tok_success_123"
  );

  expect(result.status).toBe("success");
  expect(result.transactionId).toBeDefined();
});

使用 Vitest 模拟函数

// src/utils/analytics.ts
export const trackEvent = (event: string, data: any) => {
  // Send to analytics service
};

// tests/component.test.ts
import { vi } from "vitest";
import * as analytics from "@/utils/analytics";

test("should track button click", () => {
  // Mock function
  const trackEventSpy = vi.spyOn(analytics, "trackEvent");

  render(<Button onClick={handleClick} />);
  fireEvent.click(screen.getByRole("button"));

  expect(trackEventSpy).toHaveBeenCalledWith("button_click", {
    buttonId: "submit",
  });
});

日期/时间模拟

// tests/date-sensitive.test.ts
import { vi } from "vitest";

test("should show correct greeting based on time", () => {
  // Mock date to morning
  vi.setSystemTime(new Date("2024-01-01 09:00:00"));

  render(<Greeting />);
  expect(screen.getByText("Good morning!")).toBeInTheDocument();

  // Mock date to evening
  vi.setSystemTime(new Date("2024-01-01 19:00:00"));

  render(<Greeting />);
  expect(screen.getByText("Good evening!")).toBeInTheDocument();

  // Restore real time
  vi.useRealTimers();
});

模块模拟

// src/lib/database.ts
export const db = {
  user: {
    findById: (id: string) => {
      // Real database query
    },
  },
};

// tests/mocks/database.ts
export const mockDb = {
  user: {
    findById: vi.fn((id: string) => ({
      id,
      name: "Mock User",
      email: "[email protected]",
    })),
  },
};

// tests/userService.test.ts
vi.mock("@/lib/database", () => ({
  db: mockDb,
}));

test("should fetch user from database", async () => {
  const user = await userService.getUser("123");

  expect(mockDb.user.findById).toHaveBeenCalledWith("123");
  expect(user.name).toBe("Mock User");
});

GraphQL 模拟

// mocks/graphql-handlers.ts
import { graphql, HttpResponse } from "msw";

export const graphqlHandlers = [
  graphql.query("GetUser", ({ variables }) => {
    return HttpResponse.json({
      data: {
        user: {
          id: variables.id,
          name: "John Doe",
          email: "[email protected]",
        },
      },
    });
  }),

  graphql.mutation("CreateUser", ({ variables }) => {
    return HttpResponse.json({
      data: {
        createUser: {
          id: Math.random().toString(),
          ...variables.input,
        },
      },
    });
  }),
];

最佳实践

  1. 使用 MSW 处理 HTTP:比模拟 fetch 更真实
  2. 集中管理测试数据:使用唯一事实来源
  3. 针对特定测试进行覆盖:为每项测试覆盖默认设置
  4. 在边界处模拟:模拟服务和 API,而非内部实现
  5. 使用真实数据:测试数据应符合生产环境
  6. 覆盖错误场景:测试失败情况
  7. 控制时序:模拟延迟以测试加载状态

输出检查清单

  • 已创建 MSW 处理器
  • 已建立测试数据约定
  • 已定义常用测试数据
  • 已添加针对特定测试的模拟覆盖
  • 已实现服务模拟
  • 已恰当使用函数间谍
  • 在需要时模拟日期/时间
  • 已模拟错误场景
  • 已添加 GraphQL 模拟(如适用)
  • 已编写模拟用法文档

评论

支持 Markdown