Remix测试策略 #
一、测试概述 #
Remix 应用测试包括单元测试、集成测试和端到端测试。
二、测试环境配置 #
2.1 安装依赖 #
bash
npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom
2.2 Vitest配置 #
创建 vitest.config.ts:
typescript
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [react(), tsconfigPaths()],
test: {
environment: "jsdom",
globals: true,
setupFiles: ["./test/setup.ts"],
},
});
2.3 测试设置 #
创建 test/setup.ts:
typescript
import "@testing-library/jest-dom";
三、组件测试 #
3.1 基本组件测试 #
tsx
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { Button } from "~/components/Button";
describe("Button", () => {
it("renders with text", () => {
render(<Button>Click me</Button>);
expect(screen.getByText("Click me")).toBeInTheDocument();
});
it("handles click", async () => {
let clicked = false;
render(<Button onClick={() => (clicked = true)}>Click</Button>);
await userEvent.click(screen.getByText("Click"));
expect(clicked).toBe(true);
});
});
3.2 表单组件测试 #
tsx
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi } from "vitest";
import { SearchForm } from "~/components/SearchForm";
describe("SearchForm", () => {
it("submits search query", async () => {
const onSearch = vi.fn();
render(<SearchForm onSearch={onSearch} />);
await userEvent.type(screen.getByPlaceholderText("搜索..."), "test");
await userEvent.click(screen.getByText("搜索"));
expect(onSearch).toHaveBeenCalledWith("test");
});
});
四、路由测试 #
4.1 Loader测试 #
tsx
import { describe, it, expect } from "vitest";
import { loader } from "~/routes/posts.$id";
describe("Post loader", () => {
it("returns post data", async () => {
const response = await loader({
params: { id: "1" },
request: new Request("http://localhost/posts/1"),
context: {},
});
const data = await response.json();
expect(data.post).toBeDefined();
});
it("throws 404 for non-existent post", async () => {
await expect(
loader({
params: { id: "non-existent" },
request: new Request("http://localhost/posts/non-existent"),
context: {},
})
).rejects.toThrow("文章不存在");
});
});
4.2 Action测试 #
tsx
import { describe, it, expect } from "vitest";
import { action } from "~/routes/posts.new";
describe("Create post action", () => {
it("creates a post", async () => {
const formData = new FormData();
formData.append("title", "Test Post");
formData.append("content", "Test content");
const request = new Request("http://localhost/posts/new", {
method: "POST",
body: formData,
});
const response = await action({
request,
params: {},
context: {},
});
expect(response.status).toBe(302);
expect(response.headers.get("Location")).toMatch(/\/posts\/\d+/);
});
});
五、集成测试 #
5.1 使用createRemixStub #
tsx
import { render, screen } from "@testing-library/react";
import { createRemixStub } from "@remix-run/testing";
import { describe, it, expect } from "vitest";
import { Post } from "~/routes/posts.$id";
describe("Post page", () => {
it("renders post", async () => {
const RemixStub = createRemixStub([
{
path: "/posts/:id",
Component: Post,
loader: () => ({
post: {
id: "1",
title: "Test Post",
content: "Test content",
},
}),
},
]);
render(<RemixStub initialEntries={["/posts/1"]} />);
expect(await screen.findByText("Test Post")).toBeInTheDocument();
});
});
5.2 表单提交测试 #
tsx
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { createRemixStub } from "@remix-run/testing";
import { describe, it, expect } from "vitest";
import { NewPost } from "~/routes/posts.new";
describe("New post page", () => {
it("creates a post", async () => {
const RemixStub = createRemixStub([
{
path: "/posts/new",
Component: NewPost,
action: () => redirect("/posts/1"),
},
]);
render(<RemixStub initialEntries={["/posts/new"]} />);
await userEvent.type(screen.getByLabelText("标题"), "Test Post");
await userEvent.type(screen.getByLabelText("内容"), "Test content");
await userEvent.click(screen.getByText("发布"));
await waitFor(() => {
expect(window.location.pathname).toBe("/posts/1");
});
});
});
六、端到端测试 #
6.1 Playwright配置 #
typescript
import { defineConfig } from "@playwright/test";
export default defineConfig({
testDir: "./e2e",
use: {
baseURL: "http://localhost:3000",
},
webServer: {
command: "npm run dev",
port: 3000,
},
});
6.2 E2E测试示例 #
typescript
import { test, expect } from "@playwright/test";
test("user can login", async ({ page }) => {
await page.goto("/login");
await page.fill('input[name="email"]', "test@example.com");
await page.fill('input[name="password"]', "password");
await page.click('button[type="submit"]');
await expect(page).toHaveURL("/dashboard");
await expect(page.locator("h1")).toContainText("欢迎");
});
test("user can create post", async ({ page }) => {
await page.goto("/posts/new");
await page.fill('input[name="title"]', "Test Post");
await page.fill('textarea[name="content"]', "Test content");
await page.click('button[type="submit"]');
await expect(page).toHaveURL(/\/posts\/\d+/);
});
七、测试工具函数 #
7.1 Mock数据 #
tsx
export function createMockPost(overrides = {}) {
return {
id: "1",
title: "Test Post",
content: "Test content",
author: {
id: "1",
name: "Test User",
},
createdAt: new Date().toISOString(),
...overrides,
};
}
export function createMockUser(overrides = {}) {
return {
id: "1",
email: "test@example.com",
name: "Test User",
...overrides,
};
}
7.2 测试工具 #
tsx
export function renderWithRemix(
component: React.ReactElement,
options = {}
) {
const RemixStub = createRemixStub([
{
path: "/",
Component: () => component,
...options,
},
]);
return render(<RemixStub initialEntries={["/"]} />);
}
八、最佳实践 #
8.1 测试命名 #
tsx
describe("Component", () => {
it("should do something when condition", () => {
// ...
});
});
8.2 测试覆盖 #
bash
vitest run --coverage
8.3 测试脚本 #
json
{
"scripts": {
"test": "vitest",
"test:coverage": "vitest run --coverage",
"test:e2e": "playwright test"
}
}
九、总结 #
本章我们学习了:
- 测试环境:Vitest配置
- 组件测试:单元测试
- 路由测试:Loader和Action测试
- 集成测试:createRemixStub
- 端到端测试:Playwright
核心要点:
- 使用Vitest进行单元和集成测试
- 使用createRemixStub测试路由
- 使用Playwright进行E2E测试
最后更新:2026-03-28