使用手工事务和分层事务进行单元测试

本文关键字:事务 单元测试 分层 | 更新日期: 2023-09-27 17:53:41

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

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

对于单元测试,我需要在事务中执行,因为大多数操作本质上是改变数据的,因此在事务之外执行它们是有问题的,因为这会改变整个基本数据。因此,我需要在这些事务周围放置一个Transaction(最后不触发提交)。

现在我有这些BL方法如何工作的两种不同的变体。一些在它们内部有事务,而另一些根本没有事务。这两种变体都会导致问题。

  • 分层事务:这里我得到错误,DTC取消分布式事务由于超时(虽然超时被设置为15分钟,它只运行了2分钟)。

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

我的问题是我能做些什么来纠正这一点,并获得单元测试与手动正常和分层事务工作?

单元测试方法示例:

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

使用手工事务和分层事务进行单元测试

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

此刻,您正在有效地编写集成测试。如果数据库不可用,那么测试将失败。这意味着测试可能很慢,但从好的方面来说,如果它们通过了,你就非常有信心你的代码可以正确地访问数据库。

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

如果你能花一些钱,你想把你的测试变成单元测试,这样它们就不会碰到数据库,那么你应该考虑研究一下TypeMock。它是一个非常强大的mock框架,可以做一些非常可怕的事情。我相信它使用分析API来拦截调用,而不是使用像Moq这样的框架使用的方法。这里有一个使用Typemock模拟SQLConnection的示例。

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

如果您采用上述方法,而不是直接注入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();
            }
        }
    }
}

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