15 KiB
15 KiB
摄影作品集网站 - 测试需求文档
1. 测试策略概述
1.1 测试目标
- 功能完整性:确保所有功能按需求正常工作
- 性能稳定性:验证系统在各种负载下的表现
- 用户体验:保证界面交互流畅自然
- 兼容性:确保跨设备、跨浏览器的一致性
- 安全性:验证数据安全和用户隐私保护
1.2 测试类型分布
- 单元测试:40%(组件、工具函数、API端点)
- 集成测试:30%(模块间交互、数据流)
- 端到端测试:20%(用户完整流程)
- 性能测试:10%(加载速度、响应时间)
2. 功能测试用例
2.1 图片管理功能
2.1.1 图片上传测试
// tests/upload.test.js
describe('图片上传功能', () => {
test('应该能上传RAW格式图片', async () => {
const file = new File([rawImageBuffer], 'test.cr2', { type: 'image/x-canon-cr2' });
const result = await uploadPhoto(file);
expect(result.status).toBe('success');
expect(result.formats).toHaveProperty('raw');
expect(result.formats).toHaveProperty('jpg');
expect(result.formats).toHaveProperty('webp');
});
test('应该能上传JPEG格式图片', async () => {
const file = new File([jpegBuffer], 'test.jpg', { type: 'image/jpeg' });
const result = await uploadPhoto(file);
expect(result.status).toBe('success');
expect(result.formats.jpg).toBeDefined();
});
test('应该拒绝不支持的文件格式', async () => {
const file = new File([textBuffer], 'test.txt', { type: 'text/plain' });
await expect(uploadPhoto(file)).rejects.toThrow('不支持的文件格式');
});
test('应该拒绝过大的文件', async () => {
const largeFile = new File([new ArrayBuffer(100 * 1024 * 1024)], 'large.jpg');
await expect(uploadPhoto(largeFile)).rejects.toThrow('文件大小超出限制');
});
});
2.1.2 图片展示测试
describe('图片展示功能', () => {
test('应该正确显示图片网格', async () => {
render(<PhotoGrid photos={mockPhotos} />);
// 验证网格布局
const grid = screen.getByTestId('photo-grid');
expect(grid).toBeInTheDocument();
// 验证图片数量
const images = screen.getAllByRole('img');
expect(images).toHaveLength(mockPhotos.length);
});
test('应该支持懒加载', async () => {
const { container } = render(<PhotoGrid photos={longPhotoList} />);
// 只有可视区域的图片应该被加载
const loadedImages = container.querySelectorAll('img[src]');
expect(loadedImages.length).toBeLessThan(longPhotoList.length);
});
test('应该响应式适配不同屏幕', () => {
// 桌面端
Object.defineProperty(window, 'innerWidth', { value: 1200 });
render(<PhotoGrid photos={mockPhotos} />);
expect(screen.getByTestId('photo-grid')).toHaveClass('lg:grid-cols-4');
// 移动端
Object.defineProperty(window, 'innerWidth', { value: 600 });
render(<PhotoGrid photos={mockPhotos} />);
expect(screen.getByTestId('photo-grid')).toHaveClass('grid-cols-2');
});
});
2.2 时间线功能测试
describe('时间线功能', () => {
test('应该按年份正确分组', async () => {
const timelineData = await getTimelineData();
expect(timelineData.groups).toBeDefined();
expect(timelineData.groups[0].year).toBe(2024);
expect(timelineData.groups[0].months).toHaveProperty('1');
});
test('应该支持年份切换', async () => {
render(<Timeline data={mockTimelineData} />);
const yearButton = screen.getByText('2023');
fireEvent.click(yearButton);
await waitFor(() => {
expect(screen.getByTestId('timeline-content')).toHaveTextContent('2023');
});
});
test('应该显示正确的照片数量统计', () => {
render(<Timeline data={mockTimelineData} />);
const monthSummary = screen.getByTestId('month-summary-1');
expect(monthSummary).toHaveTextContent('15张照片');
});
});
2.3 收藏夹功能测试
describe('收藏夹功能', () => {
test('应该能创建新收藏夹', async () => {
render(<CollectionManager />);
const createButton = screen.getByText('创建收藏夹');
fireEvent.click(createButton);
const nameInput = screen.getByLabelText('收藏夹名称');
fireEvent.change(nameInput, { target: { value: '风景摄影' } });
const saveButton = screen.getByText('保存');
fireEvent.click(saveButton);
await waitFor(() => {
expect(screen.getByText('风景摄影')).toBeInTheDocument();
});
});
test('应该能添加照片到收藏夹', async () => {
render(<PhotoGrid photos={mockPhotos} collections={mockCollections} />);
const photo = screen.getAllByTestId('photo-item')[0];
fireEvent.contextMenu(photo);
const addToCollection = screen.getByText('添加到收藏夹');
fireEvent.click(addToCollection);
const collection = screen.getByText('风景摄影');
fireEvent.click(collection);
await waitFor(() => {
expect(screen.getByText('已添加到收藏夹')).toBeInTheDocument();
});
});
});
3. 性能测试
3.1 图片加载性能测试
// tests/performance/imageLoading.test.js
describe('图片加载性能', () => {
test('首屏图片应在2秒内加载完成', async () => {
const startTime = performance.now();
render(<PhotoGrid photos={mockPhotos.slice(0, 8)} />);
await waitFor(() => {
const images = screen.getAllByRole('img');
images.forEach(img => {
expect(img).toHaveAttribute('src');
});
});
const loadTime = performance.now() - startTime;
expect(loadTime).toBeLessThan(2000);
});
test('大图片应该有渐进式加载', async () => {
render(<LightboxImage src="large-image.jpg" placeholder="thumb.jpg" />);
const img = screen.getByRole('img');
// 初始应显示占位图
expect(img).toHaveAttribute('src', expect.stringContaining('thumb.jpg'));
await waitFor(() => {
// 加载完成后应显示高清图
expect(img).toHaveAttribute('src', expect.stringContaining('large-image.jpg'));
});
});
});
3.2 API响应时间测试
describe('API性能测试', () => {
test('获取照片列表API应在500ms内响应', async () => {
const startTime = Date.now();
const response = await fetch('/api/photos?limit=20');
const data = await response.json();
const responseTime = Date.now() - startTime;
expect(response.status).toBe(200);
expect(responseTime).toBeLessThan(500);
expect(data.data).toHaveLength(20);
});
test('图片上传API应支持大文件', async () => {
const largeFile = new File([new ArrayBuffer(20 * 1024 * 1024)], 'large.jpg');
const formData = new FormData();
formData.append('photo', largeFile);
const startTime = Date.now();
const response = await fetch('/api/photos', {
method: 'POST',
body: formData
});
const uploadTime = Date.now() - startTime;
expect(response.status).toBe(201);
expect(uploadTime).toBeLessThan(30000); // 30秒内完成
});
});
4. 兼容性测试
4.1 浏览器兼容性测试
// 使用Playwright进行跨浏览器测试
const { test, devices } = require('@playwright/test');
test.describe('浏览器兼容性', () => {
['chromium', 'firefox', 'webkit'].forEach(browserName => {
test(`应该在${browserName}中正常显示`, async ({ browser }) => {
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('/');
// 验证关键元素存在
await expect(page.locator('[data-testid="photo-grid"]')).toBeVisible();
await expect(page.locator('nav')).toBeVisible();
// 验证图片加载
const firstImage = page.locator('img').first();
await expect(firstImage).toBeVisible();
await context.close();
});
});
});
4.2 设备响应式测试
describe('响应式设计测试', () => {
const devices = [
{ name: 'iPhone 12', width: 390, height: 844 },
{ name: 'iPad', width: 768, height: 1024 },
{ name: 'Desktop', width: 1920, height: 1080 }
];
devices.forEach(device => {
test(`应该在${device.name}上正确显示`, async () => {
await page.setViewportSize({
width: device.width,
height: device.height
});
await page.goto('/');
// 验证导航栏适配
if (device.width < 768) {
await expect(page.locator('[data-testid="mobile-menu"]')).toBeVisible();
} else {
await expect(page.locator('[data-testid="desktop-nav"]')).toBeVisible();
}
// 验证网格列数
const grid = page.locator('[data-testid="photo-grid"]');
const expectedCols = device.width < 768 ? 2 : device.width < 1024 ? 3 : 4;
await expect(grid).toHaveClass(new RegExp(`grid-cols-${expectedCols}`));
});
});
});
5. 用户体验测试
5.1 交互流程测试
describe('用户交互流程', () => {
test('完整的照片浏览流程', async () => {
await page.goto('/');
// 1. 点击照片打开灯箱
const firstPhoto = page.locator('[data-testid="photo-item"]').first();
await firstPhoto.click();
await expect(page.locator('[data-testid="lightbox"]')).toBeVisible();
// 2. 使用键盘导航
await page.keyboard.press('ArrowRight');
await expect(page.locator('[data-testid="photo-counter"]')).toContainText('2 / ');
// 3. 缩放功能
await page.locator('[data-testid="lightbox-image"]').click();
await expect(page.locator('[data-testid="lightbox-image"]')).toHaveClass(/zoomed/);
// 4. 关闭灯箱
await page.keyboard.press('Escape');
await expect(page.locator('[data-testid="lightbox"]')).not.toBeVisible();
});
test('移动端手势操作', async () => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('/');
const firstPhoto = page.locator('[data-testid="photo-item"]').first();
await firstPhoto.click();
// 模拟左滑手势
const lightbox = page.locator('[data-testid="lightbox-image"]');
await lightbox.touchAction([
{ action: 'touchStart', x: 300, y: 400 },
{ action: 'touchMove', x: 100, y: 400 },
{ action: 'touchEnd' }
]);
await expect(page.locator('[data-testid="photo-counter"]')).toContainText('2 / ');
});
});
5.2 可访问性测试
describe('可访问性测试', () => {
test('应该支持键盘导航', async () => {
await page.goto('/');
// Tab键导航
await page.keyboard.press('Tab');
await expect(page.locator(':focus')).toBeVisible();
// 回车键激活
await page.keyboard.press('Enter');
// 验证预期的交互结果
});
test('应该有正确的ARIA标签', async () => {
await page.goto('/');
const grid = page.locator('[data-testid="photo-grid"]');
await expect(grid).toHaveAttribute('role', 'grid');
const photos = page.locator('[data-testid="photo-item"]');
await expect(photos.first()).toHaveAttribute('role', 'gridcell');
});
test('应该支持屏幕阅读器', async () => {
await page.goto('/');
const firstPhoto = page.locator('[data-testid="photo-item"]').first();
await expect(firstPhoto).toHaveAttribute('aria-label');
const img = firstPhoto.locator('img');
await expect(img).toHaveAttribute('alt');
});
});
6. 自动化测试配置
6.1 Jest配置
// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
moduleNameMapping: {
'^@/(.*)$': '<rootDir>/src/$1',
'^@/components/(.*)$': '<rootDir>/src/components/$1'
},
testMatch: [
'<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
'<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}'
],
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.d.ts',
'!src/index.js'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
};
6.2 Playwright配置
// playwright.config.js
module.exports = {
testDir: './e2e',
timeout: 30000,
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
video: 'retain-on-failure'
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] }
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] }
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] }
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] }
}
]
};
7. 性能监控和测试
7.1 Lighthouse CI集成
// lighthouse.config.js
module.exports = {
ci: {
collect: {
url: ['http://localhost:3000', 'http://localhost:3000/gallery'],
numberOfRuns: 3
},
assert: {
assertions: {
'categories:performance': ['error', { minScore: 0.9 }],
'categories:accessibility': ['error', { minScore: 0.9 }],
'categories:best-practices': ['error', { minScore: 0.9 }],
'categories:seo': ['error', { minScore: 0.9 }]
}
}
}
};
7.2 负载测试
// load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export let options = {
stages: [
{ duration: '2m', target: 100 }, // 2分钟内增加到100用户
{ duration: '5m', target: 100 }, // 维持100用户5分钟
{ duration: '2m', target: 200 }, // 增加到200用户
{ duration: '5m', target: 200 }, // 维持200用户5分钟
{ duration: '2m', target: 0 }, // 逐步减少到0
],
};
export default function() {
let response = http.get('http://localhost:3000/api/photos');
check(response, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
});
sleep(1);
}
8. 测试报告和持续集成
8.1 GitHub Actions配置
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm run test:coverage
- name: Run E2E tests
run: npm run test:e2e
- name: Run Lighthouse CI
run: npm run lighthouse:ci
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
8.2 测试覆盖率报告
// package.json scripts
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:e2e": "playwright test",
"lighthouse:ci": "lhci autorun"
}
}