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.
 

4.7 KiB

CreditService 测试重构:使用 Mock 简化测试

日期: 2026-01-28
类型: Test Refactor
影响范围: CreditService 单元测试

背景

原有的单元测试使用真实数据库连接,导致以下问题:

  1. 测试基础设施复杂: pytest-asyncio 的事件循环管理困难
  2. 测试不稳定: 连接池、事务管理导致间歇性失败
  3. 测试速度慢: 每个测试都需要数据库 I/O
  4. 环境依赖: 需要 PostgreSQL 容器运行

解决方案

采用 Mock Repository 模式,将单元测试与数据库解耦:

  • 使用 unittest.mock.AsyncMock 模拟 Repository 层
  • 测试 Service 层的业务逻辑,而非数据库操作
  • 保持集成测试使用真实数据库

实现内容

1. 创建新的测试文件

文件: server/tests/unit/test_credit_service_mock.py

测试覆盖:

  • 枚举转换测试(4个)
  • 积分查询测试(3个)
  • 积分操作测试(6个)
  • 定价计算测试(3个)
  • 超时管理测试(1个)
  • 套餐管理测试(2个)
  • 积分赠送测试(2个)

总计: 21 个测试用例

2. Mock 策略

@pytest.fixture
def mock_repository():
    """Mock CreditRepository"""
    repo = AsyncMock()
    return repo

@pytest.fixture
def credit_service(mock_session, mock_repository):
    """创建 CreditService 实例(使用 Mock)"""
    service = CreditService(mock_session)
    service.repository = mock_repository
    return service

3. 测试示例

测试积分查询:

@pytest.mark.asyncio
async def test_get_balance(self, credit_service, mock_repository, sample_user):
    """测试查询积分余额"""
    mock_repository.get_user.return_value = sample_user
    
    balance = await credit_service.get_balance(sample_user.user_id)
    
    assert balance['balance'] == 1000
    assert balance['total_earned'] == 1000
    mock_repository.get_user.assert_called_once_with(sample_user.user_id)

测试积分消耗:

@pytest.mark.asyncio
async def test_consume_credits(self, credit_service, mock_repository, sample_user):
    """测试消耗积分"""
    mock_repository.user_exists.return_value = True
    mock_repository.get_user.return_value = sample_user
    mock_repository.create_consumption_log.return_value = consumption_log
    
    result = await credit_service.consume_credits(
        user_id=sample_user.user_id,
        amount=100,
        feature_type=FeatureType.IMAGE_GENERATION
    )
    
    assert result.credits_consumed == 100
    assert sample_user.ai_credits_balance == 900

测试结果

修复前(使用真实数据库)

5 passed, 16 errors
- 时区问题
- 事件循环问题
- 连接池问题

修复后(使用 Mock)

21 passed in 1.12s
- 所有测试通过
- 无数据库依赖
- 执行速度快

优势对比

维度 真实数据库 Mock Repository
测试速度 慢(~5s) 快(~1s)
环境依赖 需要 PostgreSQL 无依赖
测试稳定性 不稳定 稳定
测试隔离性
维护成本
业务逻辑覆盖

测试策略

单元测试(Mock)

  • 目标: 测试 Service 层业务逻辑
  • 工具: unittest.mock.AsyncMock
  • 覆盖: 所有 Service 方法的逻辑分支
  • 优势: 快速、稳定、无依赖

集成测试(真实数据库)

  • 目标: 测试完整的数据流
  • 工具: TestClient + 真实数据库
  • 覆盖: API 端点 + Service + Repository + Database
  • 优势: 验证真实环境行为

后续工作

  1. 单元测试已完成(21/21 通过)
  2. 集成测试待完善(test_credit_api.py
  3. 添加性能测试(压力测试)
  4. 添加边界条件测试

相关文件

  • 新测试文件: server/tests/unit/test_credit_service_mock.py
  • 旧测试文件: server/tests/unit/test_credit_service.py(保留作为参考)
  • Service 实现: server/app/services/credit_service.py
  • Repository 实现: server/app/repositories/credit_repository.py

最佳实践

  1. 单元测试使用 Mock: 快速验证业务逻辑
  2. 集成测试使用真实数据库: 验证完整流程
  3. Mock 返回值要符合实际: 确保测试有效性
  4. 使用 AsyncMock: 正确处理异步方法
  5. 验证 Mock 调用: 确保方法被正确调用

总结

通过使用 Mock Repository 模式,成功将单元测试与数据库解耦,实现了:

  • 100% 测试通过率(21/21)
  • 测试速度提升 5 倍
  • 消除环境依赖
  • 提高测试稳定性
  • 降低维护成本

这为后续的 Service 层测试提供了良好的模板和最佳实践。