如何模拟 SqlConnection、SqlCommand?
How to mock SqlConnection, SqlCommand?
如何对此 class 进行单元测试?或者应该如何重构才能进行单元测试?
public class DomainEventsMigrator : IDomainEventsMigrator
{
private readonly string _sourceDbConnectionString;
private readonly string _destinationDbConnectionString;
private readonly ILogger<DomainEventsMigrator> _logger;
public DomainEventsMigrator(string sourceDbConnectionString, string destinationDbConnectionString, ILogger<DomainEventsMigrator> logger)
{
_sourceDbConnectionString = sourceDbConnectionString;
_destinationDbConnectionString = destinationDbConnectionString;
_logger = logger;
}
public async Task MoveBatchAsync(MigrationBatch batch)
{
Stopwatch stopWatch = Stopwatch.StartNew();
try
{
using SqlConnection sourceConnection = new(_sourceDbConnectionString);
await sourceConnection.OpenAsync();
var query =
@"SELECT StreamId, MessageId, EventDate, EventDataType, EventPayloadJson, IsActive
FROM dbo.tblExecutionPathDomainEvents WITH (NOLOCK)
WHERE Id >= @FromId AND Id < @ToId
ORDER BY Id";
SqlCommand commandSourceData = new(query, sourceConnection);
commandSourceData.Parameters.AddWithValue("@FromId", batch.FromId);
commandSourceData.Parameters.AddWithValue("@ToId", batch.ToId);
SqlDataReader reader = await commandSourceData.ExecuteReaderAsync();
stopWatch.Stop();
_logger.LogInformation($"Read attempt successful in {stopWatch.Elapsed}, FromId = {batch.FromId}, ToId = {batch.ToId}");
stopWatch = Stopwatch.StartNew();
using SqlConnection destinationConnection = new(_destinationDbConnectionString);
await destinationConnection.OpenAsync();
using SqlBulkCopy bulkCopy = new(destinationConnection);
bulkCopy.DestinationTableName = "dbo.tblExecutionPathDomainEvents";
bulkCopy.BulkCopyTimeout = 3600;
bulkCopy.BatchSize = 1000;
try
{
await bulkCopy.WriteToServerAsync(reader);
stopWatch.Stop();
_logger.LogInformation($"Write attempt successful in {stopWatch.Elapsed}, FromId = {batch.FromId}, ToId = {batch.ToId}");
}
catch (Exception e)
{
stopWatch.Stop();
_logger.LogError(e, $"Write attempt failed in {stopWatch.Elapsed}, FromId = {batch.FromId}, ToId = {batch.ToId}");
}
finally
{
reader.Close();
}
}
catch (Exception ex)
{
stopWatch.Stop();
_logger.LogError(ex, $"Read attempt failed in {stopWatch.Elapsed}, FromId = {batch.FromId}, ToId = {batch.ToId}");
}
}
}
使用 DomainEventsMigrator 的当前设计,您无法模拟 Sql 个对象(它们在 MoveBatchAsync 中本地声明)。
你应该问问自己,为什么我真的要嘲笑他们?例如,您可以将这些对象称为某些内部行为的参与者,并通过提供面向测试的连接字符串来控制它们,例如内存数据库。但是,针对某些场景,为单元测试目的建立数据库(无论是否在内存中)可能会变得复杂,而且它更像是集成测试。
鉴于创建数据库连接被认为是“无聊”的实现细节,将您的代码分解为单独的依赖项将是解决您的问题的优雅方法。与此相一致,SqlConnection 对象应该作为依赖项运行,因此应该传递给构造函数(换句话说,外部对象将负责创建和打开连接)。并且可以模拟从外部传递的对象。
还有一些工作要做:
- SqlCommand 的 ExecuteReaderAsync 方法应该被模拟。为了实现这一点,您可以在外部世界创建 SqlCommand 对象并将其传递给构造函数(而不是传递 sourceConnection),或者调用 SqlConnection.CreateCommand(而不是实例化 Sql命令)和“覆盖”测试设置代码中的 CreateCommand。
- 至于 SqlBulkCopy 对象,看起来您唯一的选择是在外部创建它并将其传递给构造函数,然后覆盖 WriteToServerAsync 以抛出异常。
逻辑很少,这里可以单元测试,因为这种方法主要是数据库交互,一般单元测试无法测试。
我认为您可以在这里做的几件事是:
提取一个方法来创建commandSourceData
个参数。这样您就可以编写单元测试来验证是否传递了正确的参数;
提取方法进行设置 SqlBulkCopy
。同样,您可以进行单元测试,然后通过简单地检查 SqlBulkCopy
public 属性并检查它们是否具有预期值来验证设置;
提取一个方法来实际执行复制。这涉及到数据库交互,并且不容易进行单元测试。您可以尝试使用内存数据库或模拟,但从我的角度来看,这两种方法都不好。
通过使用模拟,您可以让数据库交互未经测试,而通过
使用一些数据库替换你可能会面临行为差异(我
不确定例如是否可以批量复制任何东西
来自 Sql 服务器,当然你不能使用内存中的 EF Core 之类的东西
这里)。话虽如此,我宁愿建议使用集成测试和一个真正的专用数据库
在这里进行测试,只需根据需要为测试做准备。
Reseed 和
Respawn 可以帮你管理
数据库状态。
这样您将涵盖所有逻辑,同时尽可能多地使用比集成测试更快的单元测试。
然后我不确定您是否需要测试第 3 点,因为它只会测试 SqlBulkCopy
行为,我们认为这是正确的。
如何对此 class 进行单元测试?或者应该如何重构才能进行单元测试?
public class DomainEventsMigrator : IDomainEventsMigrator
{
private readonly string _sourceDbConnectionString;
private readonly string _destinationDbConnectionString;
private readonly ILogger<DomainEventsMigrator> _logger;
public DomainEventsMigrator(string sourceDbConnectionString, string destinationDbConnectionString, ILogger<DomainEventsMigrator> logger)
{
_sourceDbConnectionString = sourceDbConnectionString;
_destinationDbConnectionString = destinationDbConnectionString;
_logger = logger;
}
public async Task MoveBatchAsync(MigrationBatch batch)
{
Stopwatch stopWatch = Stopwatch.StartNew();
try
{
using SqlConnection sourceConnection = new(_sourceDbConnectionString);
await sourceConnection.OpenAsync();
var query =
@"SELECT StreamId, MessageId, EventDate, EventDataType, EventPayloadJson, IsActive
FROM dbo.tblExecutionPathDomainEvents WITH (NOLOCK)
WHERE Id >= @FromId AND Id < @ToId
ORDER BY Id";
SqlCommand commandSourceData = new(query, sourceConnection);
commandSourceData.Parameters.AddWithValue("@FromId", batch.FromId);
commandSourceData.Parameters.AddWithValue("@ToId", batch.ToId);
SqlDataReader reader = await commandSourceData.ExecuteReaderAsync();
stopWatch.Stop();
_logger.LogInformation($"Read attempt successful in {stopWatch.Elapsed}, FromId = {batch.FromId}, ToId = {batch.ToId}");
stopWatch = Stopwatch.StartNew();
using SqlConnection destinationConnection = new(_destinationDbConnectionString);
await destinationConnection.OpenAsync();
using SqlBulkCopy bulkCopy = new(destinationConnection);
bulkCopy.DestinationTableName = "dbo.tblExecutionPathDomainEvents";
bulkCopy.BulkCopyTimeout = 3600;
bulkCopy.BatchSize = 1000;
try
{
await bulkCopy.WriteToServerAsync(reader);
stopWatch.Stop();
_logger.LogInformation($"Write attempt successful in {stopWatch.Elapsed}, FromId = {batch.FromId}, ToId = {batch.ToId}");
}
catch (Exception e)
{
stopWatch.Stop();
_logger.LogError(e, $"Write attempt failed in {stopWatch.Elapsed}, FromId = {batch.FromId}, ToId = {batch.ToId}");
}
finally
{
reader.Close();
}
}
catch (Exception ex)
{
stopWatch.Stop();
_logger.LogError(ex, $"Read attempt failed in {stopWatch.Elapsed}, FromId = {batch.FromId}, ToId = {batch.ToId}");
}
}
}
使用 DomainEventsMigrator 的当前设计,您无法模拟 Sql 个对象(它们在 MoveBatchAsync 中本地声明)。
你应该问问自己,为什么我真的要嘲笑他们?例如,您可以将这些对象称为某些内部行为的参与者,并通过提供面向测试的连接字符串来控制它们,例如内存数据库。但是,针对某些场景,为单元测试目的建立数据库(无论是否在内存中)可能会变得复杂,而且它更像是集成测试。
鉴于创建数据库连接被认为是“无聊”的实现细节,将您的代码分解为单独的依赖项将是解决您的问题的优雅方法。与此相一致,SqlConnection 对象应该作为依赖项运行,因此应该传递给构造函数(换句话说,外部对象将负责创建和打开连接)。并且可以模拟从外部传递的对象。
还有一些工作要做:
- SqlCommand 的 ExecuteReaderAsync 方法应该被模拟。为了实现这一点,您可以在外部世界创建 SqlCommand 对象并将其传递给构造函数(而不是传递 sourceConnection),或者调用 SqlConnection.CreateCommand(而不是实例化 Sql命令)和“覆盖”测试设置代码中的 CreateCommand。
- 至于 SqlBulkCopy 对象,看起来您唯一的选择是在外部创建它并将其传递给构造函数,然后覆盖 WriteToServerAsync 以抛出异常。
逻辑很少,这里可以单元测试,因为这种方法主要是数据库交互,一般单元测试无法测试。
我认为您可以在这里做的几件事是:
提取一个方法来创建
commandSourceData
个参数。这样您就可以编写单元测试来验证是否传递了正确的参数;提取方法进行设置
SqlBulkCopy
。同样,您可以进行单元测试,然后通过简单地检查SqlBulkCopy
public 属性并检查它们是否具有预期值来验证设置;提取一个方法来实际执行复制。这涉及到数据库交互,并且不容易进行单元测试。您可以尝试使用内存数据库或模拟,但从我的角度来看,这两种方法都不好。
通过使用模拟,您可以让数据库交互未经测试,而通过 使用一些数据库替换你可能会面临行为差异(我 不确定例如是否可以批量复制任何东西 来自 Sql 服务器,当然你不能使用内存中的 EF Core 之类的东西 这里)。话虽如此,我宁愿建议使用集成测试和一个真正的专用数据库 在这里进行测试,只需根据需要为测试做准备。 Reseed 和 Respawn 可以帮你管理 数据库状态。
这样您将涵盖所有逻辑,同时尽可能多地使用比集成测试更快的单元测试。
然后我不确定您是否需要测试第 3 点,因为它只会测试 SqlBulkCopy
行为,我们认为这是正确的。