Vibe Coding 测试体系:API 测试、单元测试与 e2e 测试实战指南

0 阅读4分钟

前言:测试模块是 vibe coding 过程中不可忽视的一环,能够显著提升代码的稳定性与可维护性。本文系统介绍三种主流测试方式——API 测试单元测试e2e 测试,并结合 MuseMVP 项目的真实集成方式逐一演示,帮助你在快速迭代的同时建立可靠的质量防线。

项目集成

  • 安装@playwright/testvitest@vitest/ui三个依赖
  • vitest配置
import { fileURLToPath } from "node:url";
import { defineConfig } from "vitest/config";

export default defineConfig({
  resolve: {
    alias: {
      "@": fileURLToPath(new URL("./src", import.meta.url)),
    },
  },
  test: {
    environment: "node",
    globals: true,
    include: ["tests/unit/**/*.test.ts"],
  },
});
  • playwright配置
import { defineConfig, devices } from "@playwright/test";

const baseURL =
  process.env.E2E_BASE_URL ||
  process.env.NEXT_PUBLIC_SITE_URL ||
  "http://localhost:3000";

export default defineConfig({
  testDir: "./journeys",
  fullyParallel: false,
  forbidOnly: true,
  retries: 0,
  workers: 1,
  reporter: [
    ["list"],
    [
      "html",
      { open: "never", outputFolder: "../../.test-artifacts/browser-report" },
    ],
  ],
  outputDir: "../../.test-artifacts/browser-output",
  globalTeardown: "./cleanup.ts",
  use: {
    baseURL,
    trace: "on-first-retry",
    screenshot: "only-on-failure",
    video: "retain-on-failure",
  },
  projects: [
    {
      name: "desktop-chromium",
      use: {
        ...devices["Desktop Chrome"],
      },
    },
  ],
});

单元测试

用于验证纯逻辑层的行为,例如促销码的格式处理。以下示例测试了账单模块中 schema 对输入的修剪与空值转换逻辑,无需启动完整应用即可快速执行。

import { describe, expect, test } from "vitest";
import {
  createCustomerHubSchema,
  createLaunchSchema,
  museBillingGatewayParamSchema,
} from "@/backend/api/routes/muse-billing/types";

describe("muse-billing route schemas", () => {
  test("trims discount codes and converts blanks to undefined", () => {
    const parsed = createLaunchSchema.parse({
      planProductId: "plan_pro_monthly",
      discountCode: " SAVE20 ",
    });
    const blankParsed = createLaunchSchema.parse({
      planProductId: "plan_pro_monthly",
      discountCode: "   ",
    });

    expect(parsed.discountCode).toBe("SAVE20");
    expect(blankParsed.discountCode).toBeUndefined();
  });
});

e2e测试

使用Playwright框架测试真实浏览器环境下的公共页、认证页和受保护页面

playwright添加--ui参数进行可视化验证

{
  "scripts": {
    "test:e2e": "playwright test --config=tests/e2e/playwright.muse.config.ts",
    "test:e2e:ui": "playwright test --config=tests/e2e/playwright.muse.config.ts --ui",
  }
}

落地页可见性

对公开页面进行基础冒烟测试,验证页面能够正常打开、响应状态正常、DOM 可见。

import { expect, test } from "@playwright/test";
import { ROUTE_BOOK } from "../utils/routes";

test.describe("guest-facing route smoke", () => {
  test("home page renders", async ({ page }) => {
    const response = await page.goto(ROUTE_BOOK.home);

    expect(response?.ok()).toBeTruthy();
    await expect(page.locator("body")).toBeVisible();
  });
});

埋点测试

对于需要精确验证渲染结果的页面(例如定价页),可在前端组件中添加data-testid属性作为测试锚点,无需侵入业务逻辑,即可实现精准断言。

import { expect, test } from "@playwright/test";
import { ROUTE_BOOK } from "../utils/routes";

test.describe("guest-facing route smoke", () => {
  test("pricing page renders critical pricing UI", async ({ page }) => {
    const response = await page.goto(ROUTE_BOOK.pricing);

    expect(response?.ok()).toBeTruthy();
    await expect(page.getByTestId("pricing-section")).toBeVisible();
    await expect(
      page.locator('[data-testid^="pricing-plan-cta-"]').first(),
    ).toBeVisible();
  });
});

export function PricingSection() {

 // ··· 

  return (
    <section
      id="pricing"
      className={cn("bg-muted/40", className)}
      data-testid="pricing-section"
    >
      {/* ··· */}
    section>
  );
}

后台页面

对于需要身份验证的后台页面,测试流程分两步:首先通过辅助工具创建测试用户并完成登录,再像操作真实浏览器一样验证页面内容。后续断言逻辑与公共落地页测试保持一致,复用性强。

import { expect, test } from "@playwright/test";
import { ROUTE_BOOK } from "../utils/routes";
import { establishSession, makePasswordUser } from "../utils/session";

test.describe("entry and account shell smoke", () => {
  test("unauthenticated users are redirected to login", async ({ page }) => {
    await page.goto(ROUTE_BOOK.app);

    await expect(page).toHaveURL(/\/auth\/login/);
  });

  test("password login reaches the app home page", async ({ page }) => {
    const user = await makePasswordUser({
      prefix: "e2e-login-ui",
      role: "user",
    });

    await page.goto(ROUTE_BOOK.login);
    await page.getByTestId("login-email").fill(user.email);
    await page.getByTestId("login-password").fill(user.password);
    await page.getByTestId("login-submit").click();

    await expect(page).toHaveURL(/\/app(?:\?.*)?$/);
    await expect(page.getByTestId("app-home-ready")).toBeVisible();
  });

  test("authenticated users can open general settings", async ({ page }) => {
    const user = await makePasswordUser({
      prefix: "e2e-settings",
      role: "user",
    });

    await establishSession(page, user);
    await page.goto(ROUTE_BOOK.settingsGeneral);

    await expect(page).toHaveURL(/\/app\/settings\/general(?:\?.*)?$/);
    await expect(page.getByTestId("settings-general-page")).toBeVisible();
  });
});

以上均为带 UI 的交互式测试。去掉--ui参数后,测试将在headless模式下静默运行,适合在CI环境中使用,结果如下:

PS D:\project\___test> pnpm test:e2e
[dotenv@17.2.4] injecting env (20) from .env -- tip: 📡 add observability to secrets: https://dotenvx.com/ops

[browser cleanup] Removed 3 browser scenario user(s).

> musemvp@0.0.0 test:e2e D:\project\___test
> playwright test --config=tests/e2e/playwright.muse.config.ts

[dotenv@17.2.4] injecting env (20) from .env -- tip: 🔑 add access controls to secrets: https://dotenvx.com/ops

Running 7 tests using 1 worker

[dotenv@17.2.4] injecting env (0) from .env -- tip: ✅ audit secrets and track compliance: https://dotenvx.com/ops1 …nt-entry.smoke.spec.ts:6:7 › entry and account shell smoke › unauthenticated users are redirected to login (772ms)  ✓  2 …account-entry.smoke.spec.ts:12:7 › entry and account shell smoke › password login reaches the app home page (1.8s)ℹ [DB] runtime=node source=database_url strategy=database_url_first NODE_ENV=unknown POOL_MAX=53 …t-entry.smoke.spec.ts:27:7 › entry and account shell smoke › authenticated users can open general settings (949ms)  ✓  4 …um] › tests\e2e\journeys\admin-console.smoke.spec.ts:5:5 › admin users can access the users management page (1.2s)  ✓  5 …omium] › tests\e2e\journeys\public-routes.smoke.spec.ts:5:7 › guest-facing route smoke › home page renders (732ms)  ✓  6 …eys\public-routes.smoke.spec.ts:12:7 › guest-facing route smoke › pricing page renders critical pricing UI (789ms)  ✓  7 …] › tests\e2e\journeys\public-routes.smoke.spec.ts:22:7 › guest-facing route smoke › features page renders (694ms)

[browser cleanup] Removed 3 browser scenario user(s).

  7 passed (15.1s)

To open last HTML report run:

  pnpm exec playwright show-report test-results\browser-report

API测试

用于验证 AI chat 对话接口的权限隔离行为。以下示例测试了多用户场景下的会话归属:确保用户 A 创建的会话对用户 B 不可见,同时验证所有者自身的读取权限正常工作。

import { afterAll, beforeAll, describe, expect, test } from "vitest";
import { purgeScenarioUsers } from "../utils/accounts";
import {
  ensureLocalAppReady,
  issueSignedInSession,
  sendContractRequest,
} from "./request-utils";

describe("conversation ownership stays isolated across member sessions", () => {
  beforeAll(async () => {
    await ensureLocalAppReady();
  });

  afterAll(async () => {
    await purgeScenarioUsers();
  });

  test("one user cannot fetch or delete another user's conversation", async () => {
    const userA = await issueSignedInSession({
      prefix: "ownership-a",
    });
    const userB = await issueSignedInSession({
      prefix: "ownership-b",
    });

    const createResponse = await sendContractRequest(
      "POST",
      "/api/aichat/conversations",
      {
        cookies: userA.cookies,
      },
    );

    expect(createResponse.status).toBe(200);

    const createdPayload = (await createResponse.json()) as {
      conversation: { id: string };
    };
    const conversationId = createdPayload.conversation.id;

    const foreignRead = await sendContractRequest(
      "GET",
      `/api/aichat/conversations/${conversationId}`,
      {
        cookies: userB.cookies,
      },
    );
    expect(foreignRead.status).toBe(404);

    const foreignDelete = await sendContractRequest(
      "DELETE",
      `/api/aichat/conversations/${conversationId}`,
      {
        cookies: userB.cookies,
      },
    );
    expect(foreignDelete.status).toBe(404);

    const ownerRead = await sendContractRequest(
      "GET",
      `/api/aichat/conversations/${conversationId}`,
      {
        cookies: userA.cookies,
      },
    );
    expect(ownerRead.status).toBe(200);
  });
});

测试是 vibe coding 稳定运行的最后一道保障。

API 测试、单元测试、e2e 测试三层覆盖,缺一不可。希望本文的实战示例能直接用进你的项目中。

MuseMVP SaaS 模板已将上述三种测试模块完整集成,开箱即用,无需从零搭建。

公众号:尼采般地抒情