数据库模拟测试不值得


对于数据访问层,真实的数据库测试至关重要。验证外键、引用完整性和数据库事务有助于确保基础保持牢固。在服务层,真实的数据库测试揭示了业务逻辑如何与数据交互

真实数据库测试的重要性
考虑一个简单的示例,您在应用程序中创建了一个用户。 一种方法是使用模拟,而另一种方法是使用真实数据库:

# Mocked approach
it 'creates a user (mocked)' do
  user_repo = double('UserRepository')
  expect(user_repo).to receive(:create).with(
    email: 'test@example.com',
    status: 'active'
  ).and_return(User.new)

  service = UserService.new(user_repo)
  service.create_user('test@example.com')
end

# Real database approach
it 'creates a user (real database)' do
  service = UserService.new
  result = service.create_user('test@example.com')

  expect(User.find(result.id)).to be_present
  expect(User.find(result.id).status).to eq('active')
end

真正的数据库方法可以揭示数据完整性或约束方面的潜在问题。它还可能突出显示应用程序如何处理默认值和索引。通过尽早发现这些问题,您可以节省调试时间并降低在生产过程中发现它们太晚的风险。

确保测试的未来前景
随着时间的推移,新功能或架构更改可能会影响应用程序与数据库的交互方式。当数据库被模拟时,这些更改可能会被忽视。使用真实数据库的测试可以捕获由新验证、数据类型修改或时间戳精度更改引发的错误。如果引入了新的状态验证或以不同的方式处理状态时间戳,以下测试可能会很快失败:

RSpec.describe OrderService do
  it 'handles order creation with status tracking' do
    order = described_class.create_order(
      user_id: user.id,
      amount: 100
    )

    expect(order.status_changes).to include(
      from: nil,
      to: 'pending'
    )
    expect(order.status_changed_at).to be_present
  end
end

由于它与真实数据库交互,因此此测试可以防止代码与实际模式不匹配。

维护现实的数据库状态
当您测试帐户余额或交易总额是否计算正确时,真实数据库可能会暴露并发性、隔离性和聚合问题。例如:

RSpec.describe AccountBalanceService do
  it 'calculates correct balance after transactions' do
    account = create(:account)
    create(:transaction, account: account, amount: 100)
    create(:transaction, account: account, amount: -30)

    balance = described_class.calculate_balance(account)

    expect(balance).to eq(70)
    expect(account.transactions.count).to eq(2)
  end
end

这种方法可以确保事务处理和聚合计算在发展过程中保持准确性,从而捕捉模拟可能掩盖的数据一致性问题。

了解测试中的服务边界
许多应用程序都有多个层,例如控制器、服务、存储库和外部服务集成。每层都可以专注于自己的职责:

RSpec.describe OrderProcessingService do
  it 'creates order with proper database state' do
    service = described_class.new
    result = service.create_order(user_id: 1, amount: 100)

    expect(Order.find(result.id)).to be_present
    expect(Order.find(result.id).status).to eq('pending')
  end
end

RSpec.describe OrdersController do
  let(:order_service) { instance_double(OrderProcessingService) }

  it 'delegates to order service' do
    expect(order_service).to receive(:create_order)
      .with(user_id: '1', amount: 100)
      .and_return(OpenStruct.new(id: 1))

    post '/orders', params: { user_id: '1', amount: 100 }
    expect(response).to be_successful
  end
end

通过允许服务层使用真实数据库,同时控制器模拟服务,您可以隔离测试问题。这可让您的测试保持专注,并确保在真正处理数据库交互的层中验证数据库交互。

总之
依赖模拟进行数据库调用很诱人。模拟速度更快,而且通常感觉更直接。但是,针对真实数据库进行测试可以发现应用程序成熟后可能出现的隐藏陷阱。诸如唯一约束违规、默认值处理甚至性能瓶颈等问题可能只有在针对实际数据执行代码时才会浮现出来。

平衡真实数据库测试和模拟
真实数据库测试对于存储库方法、复杂的数据关系和性能敏感场景尤其重要。同时,模拟对于验证更高级别的编排和外部服务交互仍然很有价值。控制器可以专注于传入和传出数据,而不必担心数据库操作或第三方调用的复杂性。