xUnit非静态MemberData

时间:2017-12-23 17:26:06

标签: c# unit-testing xunit xunit.net

我有以下DatabaseFixture,这对我到目前为止所创建的所有测试都有效。我使用这个fixture进行集成测试,所以我可以在数据库模式结构上做出真正的断言。

public class DatabaseFixture : IDisposable
{
    public IDbConnection Connection => _connection.Value;
    private readonly Lazy<IDbConnection> _connection;

    public DatabaseFixture()
    {
        var environment = Environment.GetEnvironmentVariable("ASPNET_ENVIRONMENT") ?? "Development";
        var configuration = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("AppSettings.json", optional: false, reloadOnChange: true)
            .AddJsonFile($"AppSettings.{environment}.json", optional: true, reloadOnChange: true)
            .Build();

        _connection = new Lazy<IDbConnection>(() =>
        {
            var connection = new MySqlConnection(configuration["ConnectionStrings:MyDatabase"]);
            connection.Open();
            return connection;
        });
    }

    public void Dispose()
    {
        Connection?.Dispose();
    }
}

[CollectionDefinition("Database Connection Required")]
public class DatabaseConnectionFixtureCollection : ICollectionFixture<DatabaseFixture>
{
}

我面临的问题是我现在需要使用数据库中的表中的每条记录来调用MyDataIsAccurate(...)之类的测试方法。 xUnit提供了[MemberData]属性,这正是我所需要的,但它需要一组静态的可枚举数据。 xUnit是否提供了一种静态共享DatabaseFixture连接实例的简洁方法,或者我只是需要将其吸收并暴露同一连接实例的静态变量?

[Collection("Database Connection Required")]
public class MyTests
{
    protected DatabaseFixture Database { get; }

    // ERROR: Can't access instance of DatabaseFixture from static context...
    public static IEnumerable<object[]> MyData => Database.Connection.Query("SELECT * FROM table).ToList();

    public MyTests(DatabaseFixture databaseFixture)
    {
        Database = databaseFixture;
    }

    [Theory]
    [IntegrationTest]
    [MemberData(nameof(MyData))]
    public void MyDataIsAccurate(int value1, string value2, string value3)
    {
        // Assert some stuff about the data...
    }
}

2 个答案:

答案 0 :(得分:6)

您无法从提供测试用例的代码(无论是MemberData属性还是ClassData实现还是自定义DataAttribute子类访问fixture。

原因

Xunit创建一个AppDomain,其中包含测试用例的所有数据。它在测试发现时使用所有这些数据构建此AppDomain。也就是说,在构建测试程序集之后,IEnumerable<object[]> s位于Xunit进程的内存中,并且它们正坐在那里等待测试运行。这使得不同的测试用例能够在visual studio中的测试资源管理器中显示为不同的测试。即使它基于MemberData - 基于Theory,这些单独的测试用例也会显示为单独的测试,因为它已经运行了该代码,并且AppDomain正在等待要运行的测试。另一方面,在测试RUN开始之前,不会创建灯具(无论是类灯具还是收集装置)(您可以通过在灯具的构造函数中设置断点并查看它何时被击中来验证这一点)。这是因为它们意味着保存数据库连接之类的东西,这些数据库连接不应该在它们不需要的很长一段时间内保留在内存中。因此,在创建测试用例数据时无法访问夹具,因为尚未创建夹具。

如果我推测,我猜想Xunit的设计师会故意这样做,并且即使测试发现 - 加载测试用例 - 并且因此必须来 - 它也会这样做首先不是问题。 Xunit的目标不是一个方便的测试工具。它是为了促进TDD,并且基于TDD的方法将允许任何人只使用他们的本地开发工具来获取解决方案并运行并传递其他人正在运行的同一组测试,而不需要包含测试用例数据的某些记录。预先加载到本地数据库中。

请注意,我并不是说你不应该做你正在尝试的事情,只是我认为Xunit的设计师会告诉你,你的测试用例和灯具应该填充数据库,而不是相反。我认为至少值得考虑这种方法是否适合你。

解决方法#1

您的静态数据库连接可能有效,但可能会产生意想不到的后果。也就是说,如果数据库中的数据在测试发现完成后发生更改(读取:在Xunit构建测试用例之后),但在测试本身运行之前,您的测试仍将使用旧数据运行。在某些情况下,即使再次构建项目也是不够的 - 必须对其进行清理或重建,以便再次运行测试发现并更新测试用例。

此外,这首先打破了使用Xunit灯具的重点。当Xunit处理了fixture时,你可以选择:处理静态数据库连接(但是当你再次运行测试时它会消失,因为Xunit不会为下一个构建一个新的AppDomain运行),或什么也不做,在这种情况下,它可能也是测试程序集中某个服务定位器类的静态单例。

解决方法#2

您可以使用允许其进入夹具并检索测试数据的数据来参数化测试。这样做的缺点是,您不会在测试资源管理器或输出中将单独的测试用例列为单独的测试,正如您希望的Theory那样,但是它确实会加载数据。测试而不是在设置,因此打败了旧的数据&#34;问题以及连接寿命问题。

摘要

我不认为Xunit存在这样的事情。据我所知,您的选择是:让测试数据填充数据库而不是相反,或者使用永不丢弃的静态单例数据库连接,或者在测试本身中提取数据。这些都不是&#34;清洁&#34;你希望的解决方案,但我怀疑你能比其中一个好得多。

答案 1 :(得分:1)

有一种使用委托来实现所需目标的方法。这个极其简单的示例很好地说明了这一点:

using System;
using System.Collections.Generic;

using Xunit;

namespace YourNamespace
{
    public class XUnitDeferredMemberDataFixture
    {
        private static string testCase1;
        private static string testCase2;

        public XUnitDeferredMemberDataFixture()
        {
            // You would populate these from somewhere that's possible only at test-run time, such as a db
            testCase1 = "Test case 1";
            testCase2 = "Test case 2";
        }

        public static IEnumerable<object[]> TestCases
        {
            get
            {
                // For each test case, return a human-readable string, which is immediately available
                // and a delegate that will return the value when the test case is run.
                yield return new object[] { "Test case 1", new Func<string>(() => testCase1) };
                yield return new object[] { "Test case 2", new Func<string>(() => testCase2) };
            }
        }

        [Theory]
        [MemberData(nameof(TestCases))]
        public void Can_do_the_expected_thing(
            string ignoredTestCaseName, // Not used; useful as this shows up in your test runner as human-readable text
            Func<string> testCase) // Your test runner will show this as "Func`1 { Method = System.String.... }"
        {
            Assert.NotNull(testCase);

            // Do the rest of your test with "testCase" string.
        }
    }
}

在OP的情况下,您可以在XUnitDeferredMemberDataFixture构造函数中访问数据库。