在 XUnit 测试中手动处理 SqlConnection

Disposing of a SqlConnection manually in XUnit tests

我正在编写 SQL 使用 xUnit 的服务器集成测试。

我的测试是这样的:

[Fact]
public void Test()
{
    using (IDbConnection connection = new SqlConnection(_connectionString))
    {
        connection.Open();

        connection.Query(@$"...");
        //DO SOMETHING
    }
}

因为我计划在同一个 class 中创建多个测试,所以我试图避免每次都创建一个新的 SqlConnection。我知道 xUnit 框架本身会为其中的每个 运行ning 测试创建一个 class 实例。

这意味着我可以在构造函数中创建 SqlConnection 因为无论如何每次新测试都会创建一个新的 运行.

问题是处理它。在每次测试结束时手动处理 SqlConnection 是好的做法还是您发现任何问题?

如:

public class MyTestClass
{
    private const string _connectionString = "...";
    private readonly IDbConnection _connection;

    public MyTestClass()
    {
        _connection = new SqlConnection(_connectionString));
    }

    [Fact]
    public void Test()
    {
        _connection.Open();

        _connection.Query(@$"...");
        //DO SOMETHING
        
        _connection.Dispose();
    }
}

I was trying to avoid creating a new SqlConnection every time.

一遍又一遍地创建和处理新的 SqlConnection 是可以的。连接实例已释放,但基础连接已入池。在引擎盖下它可能实际上重复使用相同的连接而不关闭它,这没关系。 参见 SQL Server Connection Pooling

因此,如果这是您的顾虑,请不要担心。你在第一个例子中所做的是完全无害的。无论您的代码是结构化的,如果您在需要时创建新连接、使用它和处置它,那都很好。你可以整天这样做。

如果您担心的是重复代码,我觉得这很有帮助。我的单元测试中经常有这样的 class:

static class SqlExecution
{
    public static void ExecuteSql(string sql)
    {
        using (var connection = new SqlConnection(GetConnectionString()))
        {
            using (var command = new SqlCommand(sql, connection))
            {
                connection.Open();
                command.ExecuteNonQuery();
            }
        }
    }

    public static T ExecuteScalar<T>(string sql)
    {
        using (var connection = new SqlConnection(GetConnectionString()))
        {
            using (var command = new SqlCommand(sql, connection))
            {
                connection.Open();
                return (T)command.ExecuteScalar();
            }
        }
    }

    public static string GetConnectionString()
    {
        // This may vary
    }
}

您获取连接字符串的方式可能有所不同,这就是我将该方法留空的原因。它可能是配置文件,也可能是 hard-coded.

这涵盖了执行某事和检索值的常见场景,并允许我将所有重复的数据库代码排除在我的测试之外。测试中只有一行:

SqlExecution.ExecuteSql("UPDATE WHATEVER");

您还可以在 xUnit 中使用依赖注入,这样您就可以将一些东西写成一个实例 class 并注入它。但我怀疑这样做是否值得。

如果我发现自己编写的重复代码越来越多,那么我可能会添加 test-specific 方法。例如,测试 class 可能有一个方法可以执行某个存储过程或将参数格式化为查询并执行它。


您还可以使用 xUnit 进行依赖注入,以便像注入任何其他 class 一样将依赖注入到 class 中。有很多 。您可能会发现它很有用。也许这就是您使用 xUnit 的原因。但更简单的事情可能会完成工作。


有人肯定会说单元测试不应该与数据库对话。他们大多是对的。不过没关系。有时我们无论如何都必须这样做。也许我们需要的代码 单元测试是一个存储过程,从单元测试中执行它并验证 结果是最简单的方法。

有人可能会说我们不应该在数据库中有需要测试的逻辑,我也同意他们的看法。但我们并不总是有这种选择。如果我必须把逻辑放在 SQL 我仍然想测试它,而编写单元测试通常是最简单的方法。 (如果它能让任何人满意,我们可以称之为“集成测试”。)

这里有一些不同的东西在起作用。首先,SqlConnection 使用连接池,因此 create/dispose 单个测试中的连接应该没问题。 话虽如此,在每个测试 class 的基础上处理连接也是可行的。 XUnit 将处理 IDisposable 的测试 classes。所以两者都行。我建议在测试中 create/dispose 它们更干净。

public class MyTestClass : IDisposable
{
    const string ConnectionStr = "";
    SqlConnection conn;
    public MyTestClass()
    {
        this.conn = new SqlConnection(ConnectionStr);
    }

    public void Dispose()
    {
        this.conn?.Dispose();
        this.conn = null;
    }

    public void Test()
    {
        using (var conn = new SqlConnection(ConnectionStr))
        {
        }
    }
}