xUnit 非静态成员数据

xUnit Non-Static MemberData

我有以下 DatabaseFixture,它对我到目前为止创建的所有测试都运行良好。我使用这个夹具进行集成测试,这样我就可以对数据库模式结构做出真正的断言。

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

我面临的问题是我现在需要对数据库中 table 中的每条记录调用类似 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...
    }
}

您无法从提供测试用例的代码访问夹具(无论是 MemberData 属性 或 ClassData 实现还是自定义 DataAttribute 子class.

原因

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

如果让我推测的话,我猜想 Xunit 的设计者是故意这样做的,并且即使测试发现加载测试用例并且因此必须-先到先得不是问题。 Xunit 的目标不是成为一个方便的测试工具。它是为了促进 TDD,基于 TDD 的方法将允许任何人仅使用他们的本地开发工具和 运行 来获得解决方案,并通过与其他人 运行ning 相同的一组测试,不需要将包含测试用例数据的某些记录预加载到本地数据库中。

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

解决方法 #1

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

此外,这会破坏首先使用 Xunit 夹具的意义。当 Xunit 处理 fixture 时,您可以选择:处理静态数据库连接(但是当您再次 运行 测试时它将消失,因为 Xunit 不一定会为建立新的 AppDomain下一个 运行),或者什么都不做,在这种情况下,它也可能是测试程序集中某个服务定位器 class 上的静态单例。

解决方法 #2

您可以使用允许它转到夹具并检索测试数据的数据来参数化测试。这样做的缺点是您不会像 Theory 所希望的那样在测试资源管理器或输出中将单独的测试用例列为单独的测试,但它确实会在测试时加载数据而不是在设置时,因此解决了 "old data" 问题以及连接生命周期问题。

总结

我认为 Xunit 中不存在这样的东西。据我所知,您的选择是:让测试数据填充数据库而不是相反,或者使用永不处置的静态单例数据库连接,或者在测试本身中提取数据。 None 其中 "clean" 是您希望得到的解决方案,但我怀疑您能否获得比其中之一更好的解决方案。

有一种方法可以实现您想要的效果,即使用委托。这个极其简单的例子很好地解释了它:

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 构造函数中访问数据库。