You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

6.0 KiB

测试策略

文档版本:v1.1
最后更新:2025-01-18
状态⚠️ 规划中


目录

  1. 测试金字塔
  2. 单元测试
  3. 组件测试
  4. Hook 测试
  5. E2E 测试

1. 测试金字塔

        ┌──────────┐
        │  E2E    │  少量关键流程
        │ Tests   │
       ┌┴──────────┴┐
       │Integration │  主要业务流程
       │   Tests    │
      ┌┴────────────┴┐
      │  Unit Tests  │  工具函数、Hooks、组件
      └──────────────┘

2. 单元测试

注意:测试工具配置尚未在项目中实现,以下内容作为未来规划参考。

2.1 Vitest 配置(规划)

// vitest.config.ts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import path from "path";

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: "jsdom",
    setupFiles: ["./src/test/setup.ts"],
    include: ["src/**/*.{test,spec}.{ts,tsx}"],
    coverage: {
      reporter: ["text", "json", "html"],
      exclude: ["node_modules/", "src/test/"],
    },
  },
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
});

2.2 工具函数测试示例

// src/utils/format.test.ts
import { describe, it, expect } from "vitest";
import { formatDuration, formatFileSize } from "./format";

describe("formatDuration", () => {
  it("formats seconds to mm:ss", () => {
    expect(formatDuration(65)).toBe("01:05");
    expect(formatDuration(3661)).toBe("01:01:01");
  });

  it("handles zero", () => {
    expect(formatDuration(0)).toBe("00:00");
  });
});

describe("formatFileSize", () => {
  it("formats bytes correctly", () => {
    expect(formatFileSize(1024)).toBe("1 KB");
    expect(formatFileSize(1048576)).toBe("1 MB");
  });
});

3. 组件测试

3.1 测试示例

// src/components/features/storyboard/StoryboardItem.test.tsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { StoryboardItem } from './StoryboardItem';

const mockStoryboard = {
  id: '1',
  title: 'Test Storyboard',
  description: 'Test description',
  thumbnailUrl: '/test.jpg',
  duration: 5,
  order: 0,
};

describe('StoryboardItem', () => {
  it('renders storyboard info', () => {
    render(
      <StoryboardItem
        storyboard={mockStoryboard}
        isSelected={false}
        onSelect={() => {}}
      />
    );

    expect(screen.getByText('Test Storyboard')).toBeInTheDocument();
    expect(screen.getByText('Test description')).toBeInTheDocument();
  });

  it('calls onSelect when clicked', () => {
    const onSelect = vi.fn();
    render(
      <StoryboardItem
        storyboard={mockStoryboard}
        isSelected={false}
        onSelect={onSelect}
      />
    );

    fireEvent.click(screen.getByRole('button'));
    expect(onSelect).toHaveBeenCalledWith('1');
  });

  it('applies selected styles', () => {
    render(
      <StoryboardItem
        storyboard={mockStoryboard}
        isSelected={true}
        onSelect={() => {}}
      />
    );

    expect(screen.getByRole('button')).toHaveClass('border-primary');
  });
});

4. Hook 测试

// src/hooks/useDebounce.test.ts
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { renderHook, act } from "@testing-library/react";
import { useDebounce } from "./useDebounce";

describe("useDebounce", () => {
  beforeEach(() => {
    vi.useFakeTimers();
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it("returns initial value immediately", () => {
    const { result } = renderHook(() => useDebounce("hello", 500));
    expect(result.current).toBe("hello");
  });

  it("debounces value changes", () => {
    const { result, rerender } = renderHook(
      ({ value }) => useDebounce(value, 500),
      { initialProps: { value: "hello" } },
    );

    rerender({ value: "world" });
    expect(result.current).toBe("hello");

    act(() => {
      vi.advanceTimersByTime(500);
    });

    expect(result.current).toBe("world");
  });
});

5. E2E 测试

5.1 Playwright 配置(规划)

// playwright.config.ts
import { defineConfig, devices } from "@playwright/test";

export default defineConfig({
  testDir: "./e2e",
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: "html",
  use: {
    baseURL: "http://localhost:5173",
    trace: "on-first-retry",
  },
  projects: [
    {
      name: "chromium",
      use: { ...devices["Desktop Chrome"] },
    },
  ],
  webServer: {
    command: "npm run dev",
    url: "http://localhost:5173",
    reuseExistingServer: !process.env.CI,
  },
});

5.2 E2E 测试示例

// e2e/editor.spec.ts
import { test, expect } from "@playwright/test";

test.describe("Editor Page", () => {
  test.beforeEach(async ({ page }) => {
    await page.goto("/editor/project-1");
  });

  test("displays storyboard list", async ({ page }) => {
    await expect(page.getByTestId("storyboard-panel")).toBeVisible();
    await expect(page.getByTestId("storyboard-item")).toHaveCount(5);
  });

  test("can select a storyboard", async ({ page }) => {
    const firstItem = page.getByTestId("storyboard-item").first();
    await firstItem.click();
    await expect(firstItem).toHaveClass(/selected/);
  });

  test("plays video on play button click", async ({ page }) => {
    await page.getByRole("button", { name: "播放" }).click();
    await expect(page.getByRole("button", { name: "暂停" })).toBeVisible();
  });
});

相关文档


最后更新:2025-01-18 | 版本:v1.1