18. Тестирование
Тестирование в Remix охватывает unit-тесты (Vitest), интеграционные тесты маршрутов и E2E тесты (Playwright/Cypress).
Настройка Vitest
Заголовок раздела «Настройка Vitest»npm install -D vitest @testing-library/react @testing-library/jest-dom happy-domimport { defineConfig } from "vitest/config";import react from "@vitejs/plugin-react";import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({ plugins: [react(), tsconfigPaths()], test: { globals: true, environment: "happy-dom", setupFiles: ["./tests/setup.ts"], },});
// tests/setup.tsimport "@testing-library/jest-dom/vitest";Unit тесты утилит
Заголовок раздела «Unit тесты утилит»import { describe, it, expect } from "vitest";import { formatPrice, slugify } from "./format";
describe("formatPrice", () => { it("форматирует цену в рубли", () => { expect(formatPrice(1500)).toBe("1 500 ₽"); expect(formatPrice(99.5)).toBe("99,50 ₽"); });});
describe("slugify", () => { it("создаёт slug из строки", () => { expect(slugify("Hello World")).toBe("hello-world"); expect(slugify("React & Remix")).toBe("react-remix"); });});Тестирование loader
Заголовок раздела «Тестирование loader»// app/routes/users.$id.test.tsimport { createRequest } from "@remix-run/testing";import { loader } from "./users.$id";import { db } from "~/db.server";
// Мокируем БДvi.mock("~/db.server", () => ({ db: { user: { findUnique: vi.fn(), }, },}));
describe("loader /users/:id", () => { it("возвращает пользователя", async () => { vi.mocked(db.user.findUnique).mockResolvedValue(mockUser);
const response = await loader({ request: new Request("http://app.com/users/1"), params: { id: "1" }, context: {}, });
const data = await response.json(); expect(data.user).toEqual(mockUser); });
it("бросает 404 если пользователь не найден", async () => { vi.mocked(db.user.findUnique).mockResolvedValue(null);
await expect( loader({ request: new Request("http://app.com/users/999"), params: { id: "999" }, context: {} }) ).rejects.toThrow(); });});Тестирование action
Заголовок раздела «Тестирование action»import { action } from "./login";
describe("login action", () => { it("перенаправляет после успешного входа", async () => { const formData = new FormData(); formData.set("password", "correct-password");
const response = await action({ request: new Request("http://app.com/login", { method: "POST", body: formData, }), params: {}, context: {}, });
expect(response.status).toBe(302); expect(response.headers.get("Location")).toBe("/dashboard"); });});MSW для мокирования API
Заголовок раздела «MSW для мокирования API»npm install -D mswimport { http, HttpResponse } from "msw";
export const handlers = [ http.get("/api/users", () => { return HttpResponse.json([ { id: 1, name: "Алексей" }, { id: 2, name: "Мария" }, ]); }),];E2E тесты с Playwright
Заголовок раздела «E2E тесты с Playwright»import { test, expect } from "@playwright/test";
test("пользователь может войти в систему", async ({ page }) => { await page.goto("/login");
await page.getByLabel("Пароль").fill("password123"); await page.getByRole("button", { name: "Войти" }).click();
await expect(page).toHaveURL("/dashboard"); await expect(page.getByText("Добро пожаловать")).toBeVisible();});