---
title: 测试文档
order: 4
category: plus
---

# OrderList 订单列表 - 测试文档

## 测试概述

本文档详细说明了 OrderList 组件的测试策略、测试用例和测试方法。

## 测试环境

### 测试工具

- **Jest**: 单元测试框架
- **React Testing Library**: React 组件测试库
- **@testing-library/user-event**: 用户交互模拟
- **Mock Service Worker (MSW)**: API Mock 工具

### 测试数据准备

```
// mock/orderList.mock.ts

export const mockOrderData = {
  list: [
    {
      id: '1',
      shop_order_number: 'ORD-20260120-001',
      customer: {
        customer_name: '张三',
        phone: '13800138000',
        email: 'zhangsan@example.com',
      },
      contacts_info: {
        value: {
          first_name: '张',
          last_name: '三',
          display_name: '张三',
          phone: {
            country_calling_code: '+86',
            country_code: 'CN',
            phone: '13800138000',
          },
          email: 'zhangsan@example.com',
        },
      },
      tag: [
        { id: '1', name: 'VIP' },
        { id: '2', name: '新客' },
      ],
      delivery_type: 'shipping',
      payment_status: 'paid',
      platform: 'online_store',
      status: 'open',
      shipping_status: 'unfulfilled',
      total_amount: 199.99,
      paid_amount: 199.99,
      total_refund_amount: 0,
      product_quantity: 3,
      created_at: '2026-01-20 10:30:00',
      note: '请尽快发货',
    },
    // ... 更多测试数据
  ],
  total: 100,
  skip: 1,
  num: 10,
};
```

## 单元测试

### 1. 组件渲染测试

#### 测试：组件正常渲染

```
import { render, screen } from '@testing-library/react';
import OrderList from '../index';

describe('OrderList 组件渲染', () => {
  test('应该正确渲染组件', () => {
    render(<OrderList business_type="retail" />);

    // 验证标题存在
    expect(screen.getByText('订单列表')).toBeInTheDocument();

    // 验证搜索框存在
    expect(screen.getByPlaceholderText('搜索订单')).toBeInTheDocument();

    // 验证快捷筛选按钮存在
    expect(screen.getByText('今日订单')).toBeInTheDocument();
    expect(screen.getByText('今日待支付')).toBeInTheDocument();
  });
});
```

#### 测试：必填参数校验

```
test('应该在缺少 business_type 时抛出警告', () => {
  const consoleWarn = jest.spyOn(console, 'warn').mockImplementation();

  // @ts-ignore: 测试缺少必填参数的情况
  render(<OrderList />);

  expect(consoleWarn).toHaveBeenCalled();
  consoleWarn.mockRestore();
});
```

### 2. 表格列测试

#### 测试：表格列正确显示

```
import { getTableColumns } from '../config';
import { mockLocales } from './mock/locales.mock';

describe('表格列配置', () => {
  test('应该返回正确的列配置', () => {
    const columns = getTableColumns(mockLocales);

    expect(columns).toHaveLength(13);
    expect(columns[0].key).toBe('shop_order_number');
    expect(columns[1].key).toBe('customer');
  });

  test('订单号列应该正确渲染备注', () => {
    const columns = getTableColumns(mockLocales);
    const orderColumn = columns.find(col => col.key === 'shop_order_number');

    const rendered = orderColumn.render('ORD-001', { note: '测试备注' });
    expect(rendered.props.children).toHaveLength(2); // 订单号 + 备注标签
  });

  test('支付信息列应该根据状态显示不同颜色', () => {
    const columns = getTableColumns(mockLocales);
    const paymentColumn = columns.find(col => col.key === 'payment_status');

    const paidRender = paymentColumn.render('paid', {
      total_amount: 100,
      paid_amount: 100,
      total_refund_amount: 0,
    });

    // 验证颜色
    expect(paidRender.props.children.props.style.backgroundColor).toBe('#ABEFC6');
  });
});
```

### 3. 筛选功能测试

#### 测试：快速筛选配置

```
import { getQuickFilterList } from '../config';

describe('快速筛选配置', () => {
  test('应该返回正确的筛选配置', () => {
    const filterList = getQuickFilterList(mockLocales);

    expect(filterList).toHaveLength(2);
    expect(filterList[0].type).toBe('search');
    expect(filterList[1].type).toBe('rangePicker');
  });
});
```

#### 测试：详细筛选配置

```
import { getFilterButtonList } from '../config';

describe('详细筛选配置', () => {
  test('应该返回正确的筛选项配置', () => {
    const filterList = getFilterButtonList(mockLocales);

    // 验证筛选项数量
    expect(filterList.length).toBeGreaterThan(10);

    // 验证订单状态筛选
    const statusFilter = filterList.find(item => item.key === 'status');
    expect(statusFilter.type).toBe('select');
    expect(statusFilter.other.mode).toBe('multiple');
  });

  test('支持服务端搜索的筛选项应该配置正确', () => {
    const filterList = getFilterButtonList(mockLocales);
    const customerFilter = filterList.find(item => item.key === 'customer_id');

    expect(customerFilter.other.isSearchServer).toBe(true);
    expect(customerFilter.other.showSearch).toBe(true);
    expect(typeof customerFilter.other.options).toBe('function');
  });
});
```

### 4. 排序功能测试

```
import { getSortList } from '../config';

describe('排序配置', () => {
  test('应该返回正确的排序配置', () => {
    const sortList = getSortList(mockLocales);

    expect(sortList).toHaveLength(8);

    // 验证时间类型排序
    const timeSort = sortList.find(item => item.name === 'created_at');
    expect(timeSort.type).toBe('time');

    // 验证数字类型排序
    const numberSort = sortList.find(item => item.name === 'total_amount');
    expect(numberSort.type).toBe('number');
  });
});
```

### 5. 数据接口测试

#### 测试：参数格式化

```
import { formatParams } from '../serve';
import dayjs from 'dayjs';

describe('参数格式化', () => {
  test('应该正确转换分页参数', () => {
    const params = {
      pageNumber: 2,
      pageSize: 20,
    };

    const formatted = formatParams(params);
    expect(formatted.skip).toBe(2);
    expect(formatted.num).toBe(20);
  });

  test('应该正确转换时间范围参数', () => {
    const params = {
      orderDate: [
        dayjs('2026-01-01'),
        dayjs('2026-01-31'),
      ],
    };

    const formatted = formatParams(params);
    expect(formatted.start_time).toBe('2026-01-01');
    expect(formatted.end_time).toBe('2026-01-31');
  });

  test('应该正确转换金额范围参数', () => {
    const params = {
      total_amount: {
        min: 100,
        max: 500,
      },
    };

    const formatted = formatParams(params);
    expect(formatted.min_total_amount).toBe(100);
    expect(formatted.max_total_amount).toBe(500);
  });

  test('应该包含正确的关联数据配置', () => {
    const formatted = formatParams({});

    expect(formatted.with).toContain('customer');
    expect(formatted.with).toContain('contactsInfo');
    expect(formatted.with).toContain('tag');
  });
});
```

## 集成测试

### 1. 用户交互测试

#### 测试：搜索功能

```
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import OrderList from '../index';

describe('搜索功能', () => {
  test('用户输入关键字应该触发搜索', async () => {
    const user = userEvent.setup();
    render(<OrderList business_type="retail" />);

    const searchInput = screen.getByPlaceholderText('搜索订单');
    await user.type(searchInput, 'ORD-001');

    await waitFor(() => {
      expect(screen.getByText('ORD-20260120-001')).toBeInTheDocument();
    });
  });
});
```

#### 测试：快捷筛选按钮

```
describe('快捷筛选按钮', () => {
  test('点击"今日订单"应该筛选今天的订单', async () => {
    const user = userEvent.setup();
    render(<OrderList business_type="retail" />);

    const todayButton = screen.getByText('今日订单');
    await user.click(todayButton);

    await waitFor(() => {
      // 验证按钮激活状态
      expect(todayButton).toHaveClass('ant-btn-primary');
      expect(todayButton).not.toHaveClass('ant-btn-background-ghost');
    });
  });

  test('点击"今日待支付"应该筛选今天待支付的订单', async () => {
    const user = userEvent.setup();
    render(<OrderList business_type="retail" />);

    const todayPendingButton = screen.getByText('今日待支付');
    await user.click(todayPendingButton);

    await waitFor(() => {
      // 验证表格数据
      const rows = screen.getAllByRole('row');
      expect(rows.length).toBeGreaterThan(1); // 至少有表头和一行数据
    });
  });
});
```

#### 测试：表格排序

```
describe('表格排序', () => {
  test('点击排序按钮应该打开排序面板', async () => {
    const user = userEvent.setup();
    render(<OrderList business_type="retail" />);

    // 找到排序按钮（可能需要根据实际实现调整选择器）
    const sortButton = screen.getByRole('button', { name: /排序/i });
    await user.click(sortButton);

    await waitFor(() => {
      expect(screen.getByText('创建时间')).toBeInTheDocument();
      expect(screen.getByText('订单金额')).toBeInTheDocument();
    });
  });
});
```

#### 测试：表格筛选

```
describe('表格筛选', () => {
  test('打开筛选面板并选择订单状态', async () => {
    const user = userEvent.setup();
    render(<OrderList business_type="retail" />);

    // 打开筛选面板
    const filterButton = screen.getByRole('button', { name: /筛选/i });
    await user.click(filterButton);

    await waitFor(() => {
      expect(screen.getByText('订单状态')).toBeInTheDocument();
    });

    // 选择"已完成"状态
    const statusSelect = screen.getByLabelText('订单状态');
    await user.click(statusSelect);
    await user.click(screen.getByText('已完成'));

    // 提交筛选
    const submitButton = screen.getByRole('button', { name: /确定/i });
    await user.click(submitButton);

    await waitFor(() => {
      // 验证筛选结果
      const rows = screen.getAllByRole('row');
      expect(rows.length).toBeGreaterThan(0);
    });
  });
});
```

### 2. 分页测试

```
describe('分页功能', () => {
  test('点击下一页应该加载新数据', async () => {
    const user = userEvent.setup();
    render(<OrderList business_type="retail" />);

    await waitFor(() => {
      expect(screen.getByText('ORD-20260120-001')).toBeInTheDocument();
    });

    // 点击下一页
    const nextButton = screen.getByLabelText('Next Page');
    await user.click(nextButton);

    await waitFor(() => {
      expect(screen.getByText('ORD-20260120-011')).toBeInTheDocument();
    });
  });

  test('修改每页显示数量应该重新加载数据', async () => {
    const user = userEvent.setup();
    render(<OrderList business_type="retail" />);

    // 打开每页显示数量选择器
    const pageSizeSelector = screen.getByText('10 / page');
    await user.click(pageSizeSelector);

    // 选择 20 条/页
    await user.click(screen.getByText('20 / page'));

    await waitFor(() => {
      const rows = screen.getAllByRole('row');
      expect(rows.length).toBe(21); // 表头 + 20 行数据
    });
  });
});
```

### 3. 多语言测试

```
describe('多语言功能', () => {
  test('切换到英文应该显示英文文案', () => {
    const context = {
      engine: { props: { locale: 'en' } },
    };

    render(
      <EngineContext.Provider value={context}>
        <OrderList business_type="retail" />
      </EngineContext.Provider>
    );

    expect(screen.getByText('Order List')).toBeInTheDocument();
    expect(screen.getByPlaceholderText('Search orders')).toBeInTheDocument();
  });

  test('切换到繁体中文应该显示繁体文案', () => {
    const context = {
      engine: { props: { locale: 'zh-HK' } },
    };

    render(
      <EngineContext.Provider value={context}>
        <OrderList business_type="retail" />
      </EngineContext.Provider>
    );

    expect(screen.getByText('訂單列表')).toBeInTheDocument();
  });
});
```

## 性能测试

### 1. 渲染性能测试

```
import { render } from '@testing-library/react';
import { performance } from 'perf_hooks';

describe('渲染性能', () => {
  test('首次渲染应该在合理时间内完成', () => {
    const startTime = performance.now();

    render(<OrderList business_type="retail" />);

    const endTime = performance.now();
    const renderTime = endTime - startTime;

    // 首次渲染应该在 1000ms 内完成
    expect(renderTime).toBeLessThan(1000);
  });

  test('大量数据渲染应该在合理时间内完成', async () => {
    // Mock 大量数据
    const largeDataSet = Array.from({ length: 1000 }, (_, i) => ({
      id: `${i}`,
      shop_order_number: `ORD-${i}`,
      // ... 其他字段
    }));

    const startTime = performance.now();

    render(<OrderList business_type="retail" />);

    // 等待数据加载
    await waitFor(() => {
      expect(screen.getAllByRole('row').length).toBeGreaterThan(1);
    });

    const endTime = performance.now();
    const renderTime = endTime - startTime;

    // 大量数据渲染应该在 2000ms 内完成
    expect(renderTime).toBeLessThan(2000);
  });
});
```

### 2. 内存泄漏测试

```
describe('内存泄漏检测', () => {
  test('组件卸载应该清理所有副作用', () => {
    const { unmount } = render(<OrderList business_type="retail" />);

    // 记录初始内存使用
    const initialMemory = process.memoryUsage().heapUsed;

    // 卸载组件
    unmount();

    // 触发垃圾回收（仅在测试环境）
    if (global.gc) {
      global.gc();
    }

    // 验证内存释放
    const finalMemory = process.memoryUsage().heapUsed;
    const memoryDiff = finalMemory - initialMemory;

    // 内存增长应该在合理范围内
    expect(memoryDiff).toBeLessThan(1024 * 1024); // 1MB
  });
});
```

## 边界测试

### 1. 空数据测试

```
describe('空数据处理', () => {
  test('无订单时应该显示空状态', async () => {
    // Mock 空数据响应
    server.use(
      rest.post('/shop/order/v2/list', (req, res, ctx) => {
        return res(ctx.json({ list: [], total: 0 }));
      })
    );

    render(<OrderList business_type="retail" />);

    await waitFor(() => {
      expect(screen.getByText(/暂无数据/i)).toBeInTheDocument();
    });
  });
});
```

### 2. 错误处理测试

```
describe('错误处理', () => {
  test('API 请求失败应该显示错误提示', async () => {
    // Mock API 错误
    server.use(
      rest.post('/shop/order/v2/list', (req, res, ctx) => {
        return res(ctx.status(500));
      })
    );

    render(<OrderList business_type="retail" />);

    await waitFor(() => {
      expect(screen.getByText(/请求失败/i)).toBeInTheDocument();
    });
  });

  test('网络超时应该显示超时提示', async () => {
    // Mock 网络超时
    server.use(
      rest.post('/shop/order/v2/list', (req, res, ctx) => {
        return res(ctx.delay(10000));
      })
    );

    render(<OrderList business_type="retail" />);

    await waitFor(() => {
      expect(screen.getByText(/请求超时/i)).toBeInTheDocument();
    }, { timeout: 11000 });
  });
});
```

### 3. 特殊字符测试

```
describe('特殊字符处理', () => {
  test('应该正确显示包含特殊字符的订单号', () => {
    const specialOrderNumber = 'ORD-<script>alert("XSS")</script>';

    const columns = getTableColumns(mockLocales);
    const orderColumn = columns.find(col => col.key === 'shop_order_number');

    const rendered = orderColumn.render(specialOrderNumber, {});

    // 验证特殊字符被正确转义
    expect(rendered.props.children[0].props.children).toBe(specialOrderNumber);
  });
});
```

## 可访问性测试

```
import { axe, toHaveNoViolations } from 'jest-axe';

expect.extend(toHaveNoViolations);

describe('可访问性', () => {
  test('组件应该符合 WCAG 2.1 AA 标准', async () => {
    const { container } = render(<OrderList business_type="retail" />);

    const results = await axe(container);
    expect(results).toHaveNoViolations();
  });

  test('表格应该有正确的 ARIA 标签', () => {
    render(<OrderList business_type="retail" />);

    const table = screen.getByRole('table');
    expect(table).toHaveAttribute('aria-label');
  });

  test('按钮应该有正确的 ARIA 标签', () => {
    render(<OrderList business_type="retail" />);

    const buttons = screen.getAllByRole('button');
    buttons.forEach(button => {
      expect(button).toHaveAccessibleName();
    });
  });
});
```

## 测试覆盖率要求

- **语句覆盖率**: ≥ 80%
- **分支覆盖率**: ≥ 75%
- **函数覆盖率**: ≥ 80%
- **行覆盖率**: ≥ 80%

## 持续集成

### 运行测试

```bash
# 运行所有测试
pnpm test

# 运行特定测试文件
pnpm test orderList.test.tsx

# 运行测试并生成覆盖率报告
pnpm test:coverage

# 监听模式运行测试
pnpm test:watch
```

### 测试报告

测试完成后会生成以下报告：

- `coverage/lcov-report/index.html`: 覆盖率报告
- `test-results.xml`: JUnit 格式测试结果
- `coverage/coverage-summary.json`: 覆盖率摘要

## 测试最佳实践

1. **遵循 AAA 模式**: Arrange（准备）、Act（执行）、Assert（断言）
2. **保持测试独立**: 每个测试不应依赖其他测试
3. **使用有意义的测试名称**: 描述测试的目的和预期结果
4. **避免测试实现细节**: 测试行为而非实现
5. **适当使用 Mock**: 只 Mock 外部依赖，不 Mock 内部实现
6. **保持测试简单**: 一个测试只验证一个功能点
7. **定期更新测试**: 随着功能演进更新测试用例

## 测试清单

- [ ] 组件正常渲染
- [ ] 必填参数校验
- [ ] 表格列正确显示
- [ ] 搜索功能正常
- [ ] 快捷筛选按钮正常
- [ ] 详细筛选正常
- [ ] 排序功能正常
- [ ] 分页功能正常
- [ ] 多语言切换正常
- [ ] 空数据处理正常
- [ ] 错误处理正常
- [ ] 性能指标达标
- [ ] 无内存泄漏
- [ ] 可访问性达标
- [ ] 覆盖率达标
