【问题标题】:Unit testing with manual transactions and layered transactions使用手动事务和分层事务进行单元测试
【发布时间】:2015-06-05 08:38:49
【问题描述】:

由于一些限制,我不能使用实体框架,因此需要手动使用 SQL 连接、命令和事务。

在为调用这些数据层操作的方法编写单元测试时,我偶然发现了一些问题。

对于单元测试,我需要在事务中执行它们,因为大多数操作都按其性质更改数据,因此在事务之外执行它们是有问题的,因为这会更改整个基础数据.因此我需要围绕这些设置一个事务(最后没有提交)。

现在我有 2 种不同的变体来说明这些 BL 方法的工作原理。 一些内部有事务本身,而另一些则根本没有事务。这两种变体都会导致问题。

  • Layered Transaction:这里我收到错误消息,DTC 由于超时而取消了分布式事务(尽管超时设置为 15 分钟并且它只运行了 2 分钟)。

  • Only 1 Transaction:当我来到被调用方法中的"new SQLCommand" 行时,我得到一个关于Transaction 状态的错误。

我的问题是我可以做些什么来纠正这个问题并在手动正常和分层事务工作的情况下进行单元测试?

单元测试方法示例:

using (SqlConnection connection = new SqlConnection(Properties.Settings.Default.ConnectionString))
{
    connection.Open();
    using (SqlTransaction transaction = connection.BeginTransaction())
    {
        MyBLMethod();
    }
}

使用方法的交易示例(非常简单)

using (SqlConnection connection = new SqlConnection(Properties.Settings.Default.ConnectionString))
{
    connection.Open();
    using (SqlTransaction transaction = connection.BeginTransaction())
    {
        SqlCommand command = new SqlCommand();
        command.Connection = connection;
        command.Transaction = transaction;
        command.CommandTimeout = 900;   // Wait 15 minutes before a timeout
        command.CommandText = "INSERT ......";
        command.ExecuteNonQuery();

        // Following commands
        ....

        Transaction.Commit();
    }
}

使用方法的非交易示例

using (SqlConnection connection = new SqlConnection(Properties.Settings.Default.ConnectionString))
{
    connection.Open();

    SqlCommand command = new SqlCommand();
    command.Connection = connection;
    command.CommandTimeout = 900;   // Wait 15 minutes before a timeout
    command.CommandText = "INSERT ......";
    command.ExecuteNonQuery();
}

【问题讨论】:

  • 由于必须使用SqlConnection,所以不能进行单元测试;你正在做集成测试。因此,除了提交之外,不要做任何事情,而是使用一个测试数据库(您可以在测试运行开始时重新构建它)并只测试数据库交互。
  • 他说了什么。您应该每次都使用数据库快照创建干净的数据库和已知数据。
  • 这意味着我需要为每个单元测试运行重新创建一个完整的(可能跨越相当多 GB 的数据)数据库? b(这可能会使运行时间增加一倍或三倍,然后持续几分钟)

标签: c# sql unit-testing transactions


【解决方案1】:

从表面上看,您有几个选择,具体取决于您要测试的内容以及您花钱/更改代码库的能力。

目前,您正在有效地编写集成测试。如果数据库不可用,那么您的测试将失败。这意味着测试可能会很慢,但如果它们通过了,那么你很有信心你的代码可以正确地访问数据库。

如果您不介意访问数据库,那么更改代码/花钱的最小影响将是您允许交易完成并在数据库中验证它们。您可以通过获取数据库快照并在每次测试运行时重置数据库来做到这一点,或者通过拥有一个专用的测试数据库并以这样一种方式编写您的测试,以便它们可以安全地一遍又一遍地访问数据库然后进行验证。因此,例如,您可以插入一条带有递增 id 的记录,更新该记录,然后验证它是否可以读取。如果出现错误,您可能有更多的工作要做,但如果您不经常修改数据访问代码或数据库结构,那么这应该不是什么大问题。

如果您能够花一些钱,并且希望将您的测试真正变成单元测试,这样它们就不会进入数据库,那么您应该考虑使用 TypeMock。这是一个非常强大的模拟框架,可以做一些非常可怕的事情。我相信它使用分析 API 来拦截调用,而不是使用像 Moq 这样的框架使用的方法。有一个使用 Typemock 模拟 SQLConnection here 的示例。

如果您没有钱花/您能够更改代码并且不介意继续依赖数据库,那么您需要寻找某种方法来在测试代码和测试代码之间共享数据库连接您的数据访问方法。想到的两种方法是将连接信息注入到类中,或者通过注入可以访问连接信息的工厂来使其可用(在这种情况下,您可以在返回连接的测试期间注入工厂的模拟你要)。

如果您采用上述方法,而不是直接注入SqlConnection,请考虑注入一个也负责事务的包装类。比如:

public class MySqlWrapper : IDisposable {
    public SqlConnection Connection { get; set; }
    public SqlTransaction Transaction { get; set; }

    int _transactionCount = 0;

    public void BeginTransaction() {
        _transactionCount++;
        if (_transactionCount == 1) {
            Transaction = Connection.BeginTransaction();
        }
    }

    public void CommitTransaction() {
        _transactionCount--;
        if (_transactionCount == 0) {
            Transaction.Commit();
            Transaction = null;
        }
        if (_transactionCount < 0) {
            throw new InvalidOperationException("Commit without Begin");
        }
    }

    public void Rollback() {
        _transactionCount = 0;
        Transaction.Rollback();
        Transaction = null;
    }


    public void Dispose() {
        if (null != Transaction) {
            Transaction.Dispose();
            Transaction = null;
        }
        Connection.Dispose();
    }
}

这将阻止嵌套事务的创建和提交。

如果您更愿意重构您的代码,那么您可能希望以更可模拟的方式包装您的数据访问代码。因此,例如,您可以将核心数据库访问功能推入另一个类。根据您正在做的事情,您需要对其进行扩展,但是您最终可能会得到这样的结果:

public interface IMyQuery {
    string GetCommand();
}

public class MyInsert : IMyQuery{
    public string GetCommand() {
        return "INSERT ...";
    }
}

class DBNonQueryRunner {
    public void RunQuery(IMyQuery query) {
        using (SqlConnection connection = new SqlConnection(Properties.Settings.Default.ConnectionString)) {
            connection.Open();
            using (SqlTransaction transaction = connection.BeginTransaction()) {
                SqlCommand command = new SqlCommand();
                command.Connection = connection;
                command.Transaction = transaction;
                command.CommandTimeout = 900;   // Wait 15 minutes before a timeout
                command.CommandText = query.GetCommand();

                command.ExecuteNonQuery();

                transaction.Commit();
            }
        }
    }
}

这使您可以对更多逻辑进行单元测试,例如命令生成代码,而不必真正担心会访问数据库,并且您可以针对数据库测试一次核心数据访问代码(运行程序),而不是每次都测试要对数据库运行的命令。我仍然会为所有数据访问代码编写集成测试,但我只会在实际处理该部分代码时运行它们(以确保正确指定列名等)。

【讨论】:

  • +1 @forsvarir 所说的。您总是可以通过添加另一层间接来测试事物 :) 但是,如果您喜欢的话,还有 TypeMock。
猜你喜欢
  • 1970-01-01
  • 2022-08-21
  • 1970-01-01
  • 1970-01-01
  • 2011-08-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-07-16
相关资源
最近更新 更多