【问题标题】:How to mock SqlConnection, SqlCommand?如何模拟SqlConnection、SqlCommand?
【发布时间】:2021-09-16 17:27:08
【问题描述】:

如何对这个类进行单元测试?或者应该如何将其重构为可单元测试?

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}");
        }
    }
}

【问题讨论】:

  • 你想测试什么行为?
  • 比如说,调用 bulkCopy.WriteToServerAsync 方法时抛出的异常应该被捕获。 @彼得邦斯

标签: c# database unit-testing mocking sqlconnection


【解决方案1】:

使用当前的 DomainEventsMigrator 设计,您无法模拟 Sql 对象(它们在 MoveBatchAsync 中本地声明)。

你应该问问自己,为什么我真的想嘲笑他们?例如,您可以将这些对象称为某些内部行为的参与者,并通过提供面向测试的连接字符串来控制它们,例如内存数据库。但是,针对某些场景,为单元测试目的建立数据库(无论是否在内存中)可能会变得复杂,而且它变得更像集成测试。

鉴于创建数据库连接被认为是“无聊”的实现细节,将您的代码分解为单独的依赖项将是解决您问题的一个优雅的解决方案。与此一致,SqlConnection 对象应该作为依赖项起作用,因此应该传递给构造函数(换句话说,外部对象将负责创建和打开连接)。并且可以模拟从外部传递的对象。

还有一些工作要做:

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

【讨论】:

    【解决方案2】:

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

    在我看来,您可以在这里做的几件事是:

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

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

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

      通过使用模拟,您无需测试数据库交互,而通过 使用一些数据库替换你可能会面临行为差异(我是 不确定例如是否可以批量复制任何东西 来自 Sql Server,当然你不能像 EF Core in-memory 那样使用 这里)。话虽如此,我宁愿建议使用带有真正专用数据库的集成测试 在这里进行测试,只需根据需要准备测试即可。 像ReseedRespawn可以帮你管理 数据库状态。

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

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

    【讨论】:

      猜你喜欢
      • 2011-01-06
      • 1970-01-01
      • 2013-06-03
      • 1970-01-01
      • 2018-03-27
      • 1970-01-01
      • 2011-03-23
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多