如何模拟 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 对象应该作为依赖项运行,因此应该传递给构造函数(换句话说,外部对象将负责创建和打开连接)。并且可以模拟从外部传递的对象。

还有一些工作要做:

  1. SqlCommand 的 ExecuteReaderAsync 方法应该被模拟。为了实现这一点,您可以在外部世界创建 SqlCommand 对象并将其传递给构造函数(而不是传递 sourceConnection),或者调用 SqlConnection.CreateCommand(而不是实例化 Sql命令)和“覆盖”测试设置代码中的 CreateCommand。
  2. 至于 SqlBulkCopy 对象,看起来您唯一的选择是在外部创建它并将其传递给构造函数,然后覆盖 WriteToServerAsync 以抛出异常。

逻辑很少,这里可以单元测试,因为这种方法主要是数据库交互,一般单元测试无法测试。

我认为您可以在这里做的几件事是:

  1. 提取一个方法来创建commandSourceData个参数。这样您就可以编写单元测试来验证是否传递了正确的参数;

  2. 提取方法进行设置 SqlBulkCopy。同样,您可以进行单元测试,然后通过简单地检查 SqlBulkCopy public 属性并检查它们是否具有预期值来验证设置;

  3. 提取一个方法来实际执行复制。这涉及到数据库交互,并且不容易进行单元测试。您可以尝试使用内存数据库或模拟,但从我的角度来看,这两种方法都不好。

    通过使用模拟,您可以让数据库交互未经测试,而通过 使用一些数据库替换你可能会面临行为差异(我 不确定例如是否可以批量复制任何东西 来自 Sql 服务器,当然你不能使用内存中的 EF Core 之类的东西 这里)。话虽如此,我宁愿建议使用集成测试和一个真正的专用数据库 在这里进行测试,只需根据需要为测试做准备。 ReseedRespawn 可以帮你管理 数据库状态。

这样您将涵盖所有逻辑,同时尽可能多地使用比集成测试更快的单元测试。

然后我不确定您是否需要测试第 3 点,因为它只会测试 SqlBulkCopy 行为,我们认为这是正确的。