【问题标题】:xUnit Non-Static MemberDataxUnit 非静态成员数据
【发布时间】:2018-06-05 20:59:43
【问题描述】:

我有以下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>
{
}

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

【问题讨论】:

  • 您可能需要类或集合装置。请参阅xunit.github.io/docs/shared-context.html 了解更多信息。
  • 忘记复制它,但是(上面编辑过)但我现在已经在使用集合装置,如上图所示。问题是夹具是一个实例,我的测试类上的 MemberData 属性需要一个静态变量。我可以将我的 DatabaseFixture 内部的连接变成静态的,但我希望 xUnit 有一些东西会比诉诸于此更优雅。

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


【解决方案1】:

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

原因

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

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

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

解决方法 #1

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

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

解决方法 #2

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

总结

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

【讨论】:

  • 感谢您的详细回复。这些是一次性的集成测试,我很少会运行并验证电话号码是否仍然包含在不同的网页上,以纠正数据质量/差异。在这一点上,我正在考虑为任何需要更改的内容创建更新语句的控制台应用程序可能是可行的方法。感谢您确认 xUnit 在这方面的能力。
【解决方案2】:

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

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

【讨论】:

猜你喜欢
  • 1970-01-01
  • 2016-02-09
  • 1970-01-01
  • 2013-05-03
  • 1970-01-01
  • 1970-01-01
  • 2012-04-13
  • 1970-01-01
  • 2011-05-31
相关资源
最近更新 更多