【问题标题】:Disposing of a SqlConnection manually in XUnit tests在 XUnit 测试中手动处理 SqlConnection
【发布时间】:2020-10-14 19:34:18
【问题描述】:

我正在使用 xUnit 编写 SQL Server 集成测试。

我的测试如下所示:

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

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

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

这意味着我可以在构造函数中创建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();
    }
}

【问题讨论】:

  • 如果您不处理 SqlConnections,您最终可能会用完连接池中的连接,这将暂停测试直到它们超时或只是使它们失败。你读过Shared Context Between Tests 了吗?如果你想在一个类的多个测试之间共享一个连接,听起来你想实现一个IClassFixture<...>

标签: c# .net sql-server xunit


【解决方案1】:

我试图避免每次都创建一个新的 SqlConnection。

可以一遍又一遍地创建和处置新的 SqlConnection。连接实例被释放,但底层连接被池化。在引擎盖下,它实际上可能会重复使用相同的连接而不关闭它,这没关系。 见SQL Server Connection Pooling

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

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

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
    }
}

获取连接字符串的方式可能会有所不同,这就是我将该方法留空的原因。它可能是一个配置文件,也可能是硬编码的。

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

SqlExecution.ExecuteSql("UPDATE WHATEVER");

您还可以在 xUnit 中使用依赖注入,因此您可以编写一些东西作为实例类并注入它。但我怀疑这是否值得。

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


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


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

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

【讨论】:

  • 这是一个很好的答案,非常感谢您提供的所有细节!我相信我建议的另一种方法(手动处理连接)将是无害的并且同样有效,但是在您回答之后,我将坚持原来的方式。我还相信,当您提到可能使用依赖注入时,您的意思是将与数据库通信所需的所有逻辑(如您向我展示的 ExecuteScalar 和 ExecuteSql 方法)封装在一个不同的类中,然后将其注入我的测试类中。我对么?再次感谢!
【解决方案2】:

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

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))
        {
        }
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-08-25
    • 1970-01-01
    • 1970-01-01
    • 2017-10-31
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多