- 前言
- 正文
- DbUtil 工具类
- DbUtilTests 测试类
- 模拟测试的意义
- 总结
前言
事情是这样的,我在编写一个 ADO.NET 的工具类,然后通过 Github Copilot 生成对应的测试类,然后生成的测试类中包括 DbConnection
、DbCommand
等的一些模拟对象,然后看测试方法中还包括一些对结果的模拟,然后我就产生了一个疑问:对象和结果都是模拟出来的,那这个单元测试有什么意义?
正文
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 发出了灵魂拷问,我觉得它回答的很好,便有了这篇记录。
模拟测试的意义
- 验证逻辑是否正确
这些模拟测试主要目的是验证DbUtil
类的方法逻辑是否正确,而不是测试数据库本身。通过模拟数据库连接和命令对象,可以确保:
- 类方法正确调用
DbCommand
的方法 - 方法是否正确处理了输入参数(如 SQL 语句)
- 方法是否返回预期结果
_mockCommand.Verify(c => c.ExecuteNonQuery(), Times.Once);
上面代码就验证了ExecuteNonQuery
方法是否被调用了一次,如果在实现中有遗漏,就会测试失败。
- 隔离依赖,专注于代码逻辑
在本例中,数据库操作是一个外部依赖,直接依赖真实数据库会导致以下问题:
- 测试不稳定:数据库可能不可用,或数据状态不一致
- 测试速度慢:与数据库交互通常比内存操作慢得多
- 环境依赖:测试需要配置数据库环境,增加了复杂性
通过使用 Mock 对象,测试可以完全隔离数据库的影响,专注于验证 DbUtil
的逻辑是否正确。
- 提高测试覆盖率
通过模拟对象,可以覆盖到代码中所有与数据库交互的逻辑,而不需要实际运行 SQL 查询。如:
- 测试是否正确处理了
ExecuteNonQuery
的返回值 - 测试方法是否正确执行
这些测试可以帮助开发者在不依赖真实数据库的情况下,确保代码的行为符合预期。
- 防止回归问题
这些测试可以作为回归测试的一部分,如果将来有人修改了 DbUtil
的实现(如:改变了调用顺序或者漏掉了一些调用),这些测试会立即失败,从而提醒开发者代码修改后可能存在问题。
- 模拟复杂场景
通过 Mock 对象,可以模拟一些真实环境中难以复现的场景。如:
- 模拟数据库抛出异常
- 模拟数据库返回特定结果
- 模拟异步操作的延迟
这些场景在真实数据库中可能很难测试,但通过 Mock 可以轻松实现。
总结
这些测试通过 Mock 对象而没有直接操作真实数据库,它们的意义在于:
- 验证代码逻辑是否正确
- 隔离外部依赖,确保测试稳定
- 提高测试覆盖率
- 防止未来的代码回归问题
- 模拟复杂场景,确保代码在各种情况下都能正常工作