前言:测试模块是 vibe coding 过程中不可忽视的一环,能够显著提升代码的稳定性与可维护性。本文系统介绍三种主流测试方式——API 测试、单元测试、e2e 测试,并结合 MuseMVP 项目的真实集成方式逐一演示,帮助你在快速迭代的同时建立可靠的质量防线。
项目集成
- 安装
@playwright/test、vitest、@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/ops
✓ 1 …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=5
✓ 3 …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 模板已将上述三种测试模块完整集成,开箱即用,无需从零搭建。
公众号:尼采般地抒情