当前位置: 首页 > news >正文

使用模拟库进行测试的意义是什么?

目录
  • 前言
  • 正文
    • DbUtil 工具类
    • DbUtilTests 测试类
    • 模拟测试的意义
    • 总结

前言

事情是这样的,我在编写一个 ADO.NET 的工具类,然后通过 Github Copilot 生成对应的测试类,然后生成的测试类中包括 DbConnectionDbCommand等的一些模拟对象,然后看测试方法中还包括一些对结果的模拟,然后我就产生了一个疑问:对象和结果都是模拟出来的,那这个单元测试有什么意义?

正文

DbUtil 工具类

以下代码是 Github Copilot 生成之后,再让 Github Copilot 调整的。

public static class DbUtil
{public static int ExecuteNonQuery(DbConnection connection, string commandText, params DbParameter[] parameters){using var command = CreateCommand(connection, commandText, parameters);return command.ExecuteNonQuery();}public static DataTable ExecuteDataTable(DbConnection connection, string commandText, params DbParameter[] parameters){using var command = CreateCommand(connection, commandText, parameters);using var reader = command.ExecuteReader();var table = new DataTable();table.Load(reader);return table;}public static object? ExecuteScalar(DbConnection connection, string commandText, params DbParameter[] parameters){using var command = CreateCommand(connection, commandText, parameters);return command.ExecuteScalar();}public static DbDataReader ExecuteReader(DbConnection connection, string commandText, params DbParameter[] parameters){using var command = CreateCommand(connection, commandText, parameters);return command.ExecuteReader(CommandBehavior.CloseConnection);}private static DbCommand CreateCommand(DbConnection connection, string commandText, params DbParameter[] parameters){var command = connection.CreateCommand();command.CommandText = commandText;command.CommandType = CommandType.Text;if (parameters is { Length: > 0 }){command.Parameters.AddRange(parameters);}EnsureConnectionOpen(connection);return command;}private static void EnsureConnectionOpen(DbConnection connection){if (connection.State != ConnectionState.Open){connection.Open();}}
}

DbUtilTests 测试类

使用的测试框架是 xunit,断言库是 FluentAssertions,模拟库是 Moq,生成代码如下:

public class DbUtilTests
{private readonly Mock<DbConnection> _mockConnection;private readonly Mock<DbCommand> _mockCommand;private readonly Mock<DbDataReader> _mockReader;public DbUtilTests(){_mockConnection = new Mock<DbConnection>();_mockCommand = new Mock<DbCommand>();_mockReader = new Mock<DbDataReader>();_mockConnection.Setup(c => c.CreateCommand()).Returns(_mockCommand.Object);_mockCommand.Setup(c => c.ExecuteReader()).Returns(_mockReader.Object);_mockCommand.Setup(c => c.ExecuteReaderAsync(It.IsAny<CommandBehavior>(), default)).ReturnsAsync(_mockReader.Object);}[Fact]public void ExecuteNonQuery_ShouldExecuteCommand(){_mockCommand.Setup(c => c.ExecuteNonQuery()).Returns(1);var result = DbUtil.ExecuteNonQuery(_mockConnection.Object, "SELECT 1");result.Should().Be(1);_mockCommand.Verify(c => c.ExecuteNonQuery(), Times.Once);}[Fact]public void ExecuteScalar_ShouldReturnScalarValue(){_mockCommand.Setup(c => c.ExecuteScalar()).Returns(42);var result = DbUtil.ExecuteScalar(_mockConnection.Object, "SELECT 42");result.Should().Be(42);_mockCommand.Verify(c => c.ExecuteScalar(), Times.Once);}[Fact]public void ExecuteDataTable_ShouldReturnDataTable(){var dataTable = new DataTable();_mockReader.Setup(r => r.Read()).Returns(false);var result = DbUtil.ExecuteDataTable(_mockConnection.Object, "SELECT * FROM Test");result.Should().BeEquivalentTo(dataTable);_mockCommand.Verify(c => c.ExecuteReader(), Times.Once);}
}

随后就对 Github Copilot 发出了灵魂拷问,我觉得它回答的很好,便有了这篇记录。

模拟测试的意义

  1. 验证逻辑是否正确

这些模拟测试主要目的是验证DbUtil类的方法逻辑是否正确,而不是测试数据库本身。通过模拟数据库连接和命令对象,可以确保:

  • 类方法正确调用DbCommand的方法
  • 方法是否正确处理了输入参数(如 SQL 语句)
  • 方法是否返回预期结果
_mockCommand.Verify(c => c.ExecuteNonQuery(), Times.Once);

上面代码就验证了ExecuteNonQuery 方法是否被调用了一次,如果在实现中有遗漏,就会测试失败。

  1. 隔离依赖,专注于代码逻辑

在本例中,数据库操作是一个外部依赖,直接依赖真实数据库会导致以下问题:

  • 测试不稳定:数据库可能不可用,或数据状态不一致
  • 测试速度慢:与数据库交互通常比内存操作慢得多
  • 环境依赖:测试需要配置数据库环境,增加了复杂性

通过使用 Mock 对象,测试可以完全隔离数据库的影响,专注于验证 DbUtil 的逻辑是否正确。

  1. 提高测试覆盖率

通过模拟对象,可以覆盖到代码中所有与数据库交互的逻辑,而不需要实际运行 SQL 查询。如:

  • 测试是否正确处理了 ExecuteNonQuery 的返回值
  • 测试方法是否正确执行

这些测试可以帮助开发者在不依赖真实数据库的情况下,确保代码的行为符合预期。

  1. 防止回归问题

这些测试可以作为回归测试的一部分,如果将来有人修改了 DbUtil 的实现(如:改变了调用顺序或者漏掉了一些调用),这些测试会立即失败,从而提醒开发者代码修改后可能存在问题。

  1. 模拟复杂场景

通过 Mock 对象,可以模拟一些真实环境中难以复现的场景。如:

  • 模拟数据库抛出异常
  • 模拟数据库返回特定结果
  • 模拟异步操作的延迟

这些场景在真实数据库中可能很难测试,但通过 Mock 可以轻松实现。

总结

这些测试通过 Mock 对象而没有直接操作真实数据库,它们的意义在于:

  • 验证代码逻辑是否正确
  • 隔离外部依赖,确保测试稳定
  • 提高测试覆盖率
  • 防止未来的代码回归问题
  • 模拟复杂场景,确保代码在各种情况下都能正常工作
http://www.rkmt.cn/news/7.html

相关文章: