【问题标题】:Run code once before and after ALL tests in xUnit.net在 xUnit.net 中的所有测试之前和之后运行一次代码
【发布时间】:2022-04-14 00:53:34
【问题描述】:

TL;DR - 我正在寻找 xUnit 的相当于 MSTest 的 AssemblyInitialize(也就是我喜欢的 ONE 功能)。

特别是我正在寻找它,因为我有一些 Selenium 烟雾测试,我希望能够在没有其他依赖项的情况下运行它们。我有一个 Fixture 将为我启动 IisExpress 并在处置时将其杀死。但是在每次测试之前执行此操作会极大地膨胀运行时。

我想在测试开始时触发此代码一次,并在最后处理它(关闭进程)。我怎么能这样做呢?

即使我能够以编程方式访问诸如“当前正在运行多少测试”之类的内容,我也能弄清楚。

【问题讨论】:

  • 你的意思是 xUnit 是“通用的一组语言特定的单元测试工具,如 JUnit、NUnit 等”吗?还是将 xUnit 称为“xUnit.net,.Net 单元测试工具”?
  • 基于此表xunit.codeplex.com/… 我认为没有等价物。一种解决方法是将您的程序集初始化移动到一个单例中并从您的每个构造函数中调用它。
  • @allen - 这与我正在做的类似,但它为我提供了一个程序集初始化程序,而不是程序集拆解。这就是我询问测试计数的原因。

标签: c# automated-tests xunit.net


【解决方案1】:

截至 2015 年 11 月,xUnit 2 已经发布,因此有一种规范的方式可以在测试之间共享功能。它记录在here

基本上你需要创建一个类来做fixture:

    public class DatabaseFixture : IDisposable
    {
        public DatabaseFixture()
        {
            Db = new SqlConnection("MyConnectionString");

            // ... initialize data in the test database ...
        }

        public void Dispose()
        {
            // ... clean up test data from the database ...
        }

        public SqlConnection Db { get; private set; }
    }

带有CollectionDefinition 属性的虚拟类。 此类允许 Xunit 创建一个测试集合,并将给定的夹具用于该集合的所有测试类。

    [CollectionDefinition("Database collection")]
    public class DatabaseCollection : ICollectionFixture<DatabaseFixture>
    {
        // This class has no code, and is never created. Its purpose is simply
        // to be the place to apply [CollectionDefinition] and all the
        // ICollectionFixture<> interfaces.
    }

然后您需要在所有测试类中添加集合名称。 测试类可以通过构造函数接收fixture。

    [Collection("Database collection")]
    public class DatabaseTestClass1
    {
        DatabaseFixture fixture;

        public DatabaseTestClass1(DatabaseFixture fixture)
        {
            this.fixture = fixture;
        }
    }

它比 MsTests AssemblyInitialize 更冗长,因为你必须在每个测试类上声明它属于哪个测试集合,但它也更可模块化(并且使用 MsTests 你仍然需要在你的类上放置一个 TestClass)

注意:样本取自documentation

【讨论】:

  • 当我读到这个时:"...// 这个类没有代码,并且永远不会被创建..." 那么我真的更喜欢微软的 AssemblyInitialize 实现。更优雅。
  • @Elisabeth 我刚刚使用了这个并将[CollectionDefinition("Database collection")] 属性和ICollectionFixture&lt;DatabaseFixture&gt; 接口添加到DatabaseFixture 类,一切正常。它删除了一个空类,对我来说似乎更干净!
  • [Collection] 属性防止测试并行运行。 xUnit 中是否还有其他方法可以进行全局初始化/拆卸,以允许并行运行测试?
  • 我知道这是旧的,但是你如何在一个测试类上有多个集合?我希望它运行数据库设置代码以及我的映射代码。
  • @ITHitWebDAV 你可以使用Shimmy's Answer
【解决方案2】:

要在程序集初始化时执行代码,可以这样做(使用 xUnit 2.3.1 测试)

using Xunit.Abstractions;
using Xunit.Sdk;

[assembly: Xunit.TestFramework("MyNamespace.MyClassName", "MyAssemblyName")]

namespace MyNamespace
{   
   public class MyClassName : XunitTestFramework
   {
      public MyClassName(IMessageSink messageSink)
        :base(messageSink)
      {
        // Place initialization code here
      }

      public new void Dispose()
      {
        // Place tear down code here
        base.Dispose();
      }
   }
}

另见https://github.com/xunit/samples.xunit/tree/master/AssemblyFixtureExample

【讨论】:

  • @JonathaANTOINE 认为您应该创建一个新的 stackoverflow 问题,而不是从一个有 10000 个可能答案的开放式问题开始。
  • 调用了构造函数,但是在测试用例之后没有调用 dispose。 (xUnit 2.4.1)
  • @TonyQ 我添加了 IDisposable,然后开始调用 Dispose(MyClassName:XunitTestFramework,IDisposable)。另外,如果有人不知道“MyAssemblyName”是您的测试项目的名称,我也不知道。
【解决方案3】:

创建一个静态字段并实现一个终结器。

您可以使用 xUnit 创建一个 AppDomain 来运行您的测试程序集并在它完成时卸载它。卸载应用程序域将导致终结器运行。

我正在使用这种方法来启动和停止 IISExpress。

public sealed class ExampleFixture
{
    public static ExampleFixture Current = new ExampleFixture();

    private ExampleFixture()
    {
        // Run at start
    }

    ~ExampleFixture()
    {
        Dispose();
    }

    public void Dispose()
    {
        GC.SuppressFinalize(this);

        // Run at end
    }        
}

编辑:在您的测试中使用ExampleFixture.Current 访问fixture。

【讨论】:

  • 有趣 - appdomain 会立即处理还是需要一段时间?换句话说,如果我有两个连续的测试运行怎么办?
  • 我正在使用这种方法在我的测试开始时启动 IISExpress,并在它们全部完成后停止它。它在 ReSharper 和 Teamcity 上的 MSBuild 中运行良好。
  • @GeorgeMauer 和 Jared,也许 AppDomain Unload 事件可能更有用? (当然在关机期间所有的赌注都关闭了,但它可能只是从记忆中更可靠)
  • 这只是轶事,但自从发布此答案以来,我的构建服务器已经运行了一个月,并使用此方法完成了 300 次构建,并且运行良好。
  • 我知道这是旧的,但只是为了澄清......我是否需要从每个测试用例中调用 ExampleFixture.Current 以确保它被实例化?
【解决方案4】:

今天在框架中是不可能的。这是 2.0 计划的功能。

为了在 2.0 之前完成这项工作,您需要对框架进行重大的重新架构,或者编写自己的运行程序来识别您自己的特殊属性。

【讨论】:

  • 谢谢布拉德,只要我在这里,你知道我最近关于 VS 测试运行器的问题吗? visualstudiogallery.msdn.microsoft.com/…
  • 嘿布拉德 - 看看下面@JaredKells 的答案,你认为这种方法有什么问题吗?
  • 根据 .NET 框架,无法保证终结器何时会运行,或者它是否会运行。如果您对此感到满意,那么我想他的建议很好。 :)
  • 所以,既然 2.0 已经发布,不涉及您必须为所有测试(或调用一些通用代码)的通用基类的解决方案是实现您自己的子类XunitTestFramework,在构造函数和终结器中进行初始化,然后用 TestFrameworkAttribute 标记程序集。如果您对此不满意,终结器的替代方法是将对象添加到执行清理的基类 TestFramework 的受保护 DisposalTracker 属性中。
  • @AviCherry,现在应该是原始问题的答案。对我很有效
【解决方案5】:

我使用AssemblyFixture (NuGet)。

它的作用是提供一个IAssemblyFixture&lt;T&gt; 接口,用于替换您希望对象的生命周期作为测试程序集的任何IClassFixture&lt;T&gt;

例子:

public class Singleton { }

public class TestClass1 : IAssemblyFixture<Singleton>
{
  readonly Singletone _Singletone;
  public TestClass1(Singleton singleton)
  {
    _Singleton = singleton;
  }

  [Fact]
  public void Test1()
  {
     //use singleton  
  }
}

public class TestClass2 : IAssemblyFixture<Singleton>
{
  readonly Singletone _Singletone;
  public TestClass2(Singleton singleton)
  {
    //same singleton instance of TestClass1
    _Singleton = singleton;
  }

  [Fact]
  public void Test2()
  {
     //use singleton  
  }
}

【讨论】:

  • 如果我错了请纠正我,但值得一提的是这是 xUnit 2+
  • 除非您将[assembly: TestFramework("Xunit.Extensions.Ordering.TestFramework", "Xunit.Extensions.Ordering")] 添加到项目的类之一,否则它不起作用。 Reference
【解决方案6】:

我很生气,因为在所有 xUnit 测试结束时没有执行任务的选项。这里的一些选项不是很好,因为它们涉及更改所有测试或将它们放在一个集合下(意味着它们会同步执行)。但是 Rolf Kristensen 的回答为我提供了获取此代码所需的信息。有点长,但是你只需要把它添加到你的测试项目中,不需要其他代码更改:

using Siderite.Tests;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;

[assembly: TestFramework(
    SideriteTestFramework.TypeName,
    SideriteTestFramework.AssemblyName)]

namespace Siderite.Tests
{
    public class SideriteTestFramework : ITestFramework
    {
        public const string TypeName = "Siderite.Tests.SideriteTestFramework";
        public const string AssemblyName = "Siderite.Tests";
        private readonly XunitTestFramework _innerFramework;

        public SideriteTestFramework(IMessageSink messageSink)
        {
            _innerFramework = new XunitTestFramework(messageSink);
        }

        public ISourceInformationProvider SourceInformationProvider
        {
            set
            {
                _innerFramework.SourceInformationProvider = value;
            }
        }

        public void Dispose()
        {
            _innerFramework.Dispose();
        }

        public ITestFrameworkDiscoverer GetDiscoverer(IAssemblyInfo assembly)
        {
            return _innerFramework.GetDiscoverer(assembly);
        }

        public ITestFrameworkExecutor GetExecutor(AssemblyName assemblyName)
        {
            var executor = _innerFramework.GetExecutor(assemblyName);
            return new SideriteTestExecutor(executor);
        }

        private class SideriteTestExecutor : ITestFrameworkExecutor
        {
            private readonly ITestFrameworkExecutor _executor;
            private IEnumerable<ITestCase> _testCases;

            public SideriteTestExecutor(ITestFrameworkExecutor executor)
            {
                this._executor = executor;
            }

            public ITestCase Deserialize(string value)
            {
                return _executor.Deserialize(value);
            }

            public void Dispose()
            {
                _executor.Dispose();
            }

            public void RunAll(IMessageSink executionMessageSink, ITestFrameworkDiscoveryOptions discoveryOptions, ITestFrameworkExecutionOptions executionOptions)
            {
                _executor.RunAll(executionMessageSink, discoveryOptions, executionOptions);
            }

            public void RunTests(IEnumerable<ITestCase> testCases, IMessageSink executionMessageSink, ITestFrameworkExecutionOptions executionOptions)
            {
                _testCases = testCases;
                _executor.RunTests(testCases, new SpySink(executionMessageSink, this), executionOptions);
            }

            internal void Finished(TestAssemblyFinished executionFinished)
            {
                // do something with the run test cases in _testcases and the number of failed and skipped tests in executionFinished
            }
        }


        private class SpySink : IMessageSink
        {
            private readonly IMessageSink _executionMessageSink;
            private readonly SideriteTestExecutor _testExecutor;

            public SpySink(IMessageSink executionMessageSink, SideriteTestExecutor testExecutor)
            {
                this._executionMessageSink = executionMessageSink;
                _testExecutor = testExecutor;
            }

            public bool OnMessage(IMessageSinkMessage message)
            {
                var result = _executionMessageSink.OnMessage(message);
                if (message is TestAssemblyFinished executionFinished)
                {
                    _testExecutor.Finished(executionFinished);
                }
                return result;
            }
        }
    }
}

亮点:

  • 程序集:TestFramework 指示 xUnit 使用您的框架,该框架 默认代理的代理
  • SideriteTestFramework 还将执行器包装到自定义类中 然后包装消息接收器
  • 最后,Finished 方法被执行,带有测试列表 运行和 xUnit 消息的结果

这里可以做更多的工作。如果你想在不关心测试运行的情况下执行一些东西,你可以从 XunitTestFramework 继承并只包装消息接收器。

【讨论】:

    【解决方案7】:

    您可以使用 IUseFixture 接口来实现这一点。此外,您的所有测试都必须继承 TestBase 类。您还可以直接从测试中使用 OneTimeFixture。

    public class TestBase : IUseFixture<OneTimeFixture<ApplicationFixture>>
    {
        protected ApplicationFixture Application;
    
        public void SetFixture(OneTimeFixture<ApplicationFixture> data)
        {
            this.Application = data.Fixture;
        }
    }
    
    public class ApplicationFixture : IDisposable
    {
        public ApplicationFixture()
        {
            // This code run only one time
        }
    
        public void Dispose()
        {
            // Here is run only one time too
        }
    }
    
    public class OneTimeFixture<TFixture> where TFixture : new()
    {
        // This value does not share between each generic type
        private static readonly TFixture sharedFixture;
    
        static OneTimeFixture()
        {
            // Constructor will call one time for each generic type
            sharedFixture = new TFixture();
            var disposable = sharedFixture as IDisposable;
            if (disposable != null)
            {
                AppDomain.CurrentDomain.DomainUnload += (sender, args) => disposable.Dispose();
            }
        }
    
        public OneTimeFixture()
        {
            this.Fixture = sharedFixture;
        }
    
        public TFixture Fixture { get; private set; }
    }
    

    编辑:修复新夹具为每个测试类创建的问题。

    【讨论】:

    • 这是一个错误的建议。 ApplicationFixture 的方法将在每次测试之前和之后运行,而不仅仅是一次。始终从同一个TestBase 继承也是一个不好的建议,因为这会耗尽您的一个继承链接,并且您不能再使用它在一组相关类之间共享通用方法。事实上,这正是 IUseFixture 被发明的原因,它不必依赖继承。最后,您会注意到 xUnit 的创建者已经接受了这个问题的答案,即在 2.0 发布之前这是不可能正确完成的。
    • 实际上它不会在每次测试之前运行。当您使用 IUseFixture 时,Fixture 只会为每种类型的测试类创建一次。因此,如果您将代码放入 Fixture 构造函数中,它将只执行一次。不好的是它为每种类型的测试类运行一次,我也知道这一点。我认为这将为每个测试会话创建一个实例,但事实并非如此。我只是修改示例代码来解决这个问题。使用静态变量来存储 Fixture 实例,以确保它只会创建一次,并使用 AppDomain Unload 事件来处理 Fixture。
    【解决方案8】:

    您的构建工具是否提供这样的功能?

    在 Java 世界中,当使用Maven 作为构建工具时,我们使用适当的phases of the build lifecycle。例如。在您的情况下(使用类似 Selenium 的工具进行验收测试),可以充分利用 pre-integration-testpost-integration-test 阶段在 integration-tests 之前/之后启动/停止 web 应用程序。

    我很确定可以在您的环境中设置相同的机制。

    【讨论】:

    • 有了完整的构建系统,当然一切皆有可能。我可以很容易地用 psake 或 grunt 设置它。问题在于,Visual Studio 集成的测试运行器不会简单地使用构建系统来运行他们的测试,从我所看到的他们的代码库来看,它们由 IDE 直接调用并且他们自己直接运行任何 dll。
    【解决方案9】:

    Jared Kells 描述的方法 在 Net Core 下不起作用,因为不能保证会调用终结器。而且,事实上,上面的代码并没有调用它。请看:

    Why does the Finalize/Destructor example not work in .NET Core?

    https://github.com/dotnet/runtime/issues/16028

    https://github.com/dotnet/runtime/issues/17836

    https://github.com/dotnet/runtime/issues/24623

    所以,根据上面的好答案,这就是我最终做的事情(根据需要替换保存到文件):

    public class DatabaseCommandInterceptor : IDbCommandInterceptor
    {
        private static ConcurrentDictionary<DbCommand, DateTime> StartTime { get; } = new();
    
        public void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext) => Log(command, interceptionContext);
    
        public void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext) => Log(command, interceptionContext);
    
        public void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext) => Log(command, interceptionContext);
    
        private static void Log<T>(DbCommand command, DbCommandInterceptionContext<T> interceptionContext)
        {
            var parameters = new StringBuilder();
    
            foreach (DbParameter param in command.Parameters)
            {
                if (parameters.Length > 0) parameters.Append(", ");
                parameters.Append($"{param.ParameterName}:{param.DbType} = {param.Value}");
            }
    
            var data = new DatabaseCommandInterceptorData
            {
                CommandText = command.CommandText,
                CommandType = $"{command.CommandType}",
                Parameters = $"{parameters}",
                Duration = StartTime.TryRemove(command, out var startTime) ? DateTime.Now - startTime : TimeSpan.Zero,
                Exception = interceptionContext.Exception,
            };
    
            DbInterceptorFixture.Current.LogDatabaseCall(data);
        }
    
        public void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext) => OnStart(command);
        public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext) => OnStart(command);
        public void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext) => OnStart(command);
    
        private static void OnStart(DbCommand command) => StartTime.TryAdd(command, DateTime.Now);
    }
    
    public class DatabaseCommandInterceptorData
    {
        public string CommandText { get; set; }
        public string CommandType { get; set; }
        public string Parameters { get; set; }
        public TimeSpan Duration { get; set; }
        public Exception Exception { get; set; }
    }
    
    /// <summary>
    /// All times are in milliseconds.
    /// </summary>
    public record DatabaseCommandStatisticalData
    {
        public string CommandText { get; }
        public int CallCount { get; init; }
        public int ExceptionCount { get; init; }
        public double Min { get; init; }
        public double Max { get; init; }
        public double Mean { get; init; }
        public double StdDev { get; init; }
    
        public DatabaseCommandStatisticalData(string commandText)
        {
            CommandText = commandText;
            CallCount = 0;
            ExceptionCount = 0;
            Min = 0;
            Max = 0;
            Mean = 0;
            StdDev = 0;
        }
    
        /// <summary>
        /// Calculates k-th moment for n + 1 values: M_k(n + 1)
        /// based on the values of k, n, mkn = M_k(N), and x(n + 1).
        /// The sample adjustment (replacement of n -> (n - 1)) is NOT performed here
        /// because it is not needed for this function.
        /// Note that k-th moment for a vector x will be calculated in Wolfram as follows:
        ///     Sum[x[[i]]^k, {i, 1, n}] / n
        /// </summary>
        private static double MknPlus1(int k, int n, double mkn, double xnp1) =>
            (n / (n + 1.0)) * (mkn + (1.0 / n) * Math.Pow(xnp1, k));
    
        public DatabaseCommandStatisticalData Updated(DatabaseCommandInterceptorData data) =>
            CallCount == 0
                ? this with
                {
                    CallCount = 1,
                    ExceptionCount = data.Exception == null ? 0 : 1,
                    Min = data.Duration.TotalMilliseconds,
                    Max = data.Duration.TotalMilliseconds,
                    Mean = data.Duration.TotalMilliseconds,
                    StdDev = 0.0,
                }
                : this with
                {
                    CallCount = CallCount + 1,
                    ExceptionCount = ExceptionCount + (data.Exception == null ? 0 : 1),
                    Min = Math.Min(Min, data.Duration.TotalMilliseconds),
                    Max = Math.Max(Max, data.Duration.TotalMilliseconds),
                    Mean = MknPlus1(1, CallCount, Mean, data.Duration.TotalMilliseconds),
                    StdDev = Math.Sqrt(
                        MknPlus1(2, CallCount, Math.Pow(StdDev, 2) + Math.Pow(Mean, 2), data.Duration.TotalMilliseconds)
                        - Math.Pow(MknPlus1(1, CallCount, Mean, data.Duration.TotalMilliseconds), 2)),
                };
    
        public static string Header { get; } =
            string.Join(TextDelimiter.VerticalBarDelimiter.Key,
                new[]
                {
                    nameof(CommandText),
                    nameof(CallCount),
                    nameof(ExceptionCount),
                    nameof(Min),
                    nameof(Max),
                    nameof(Mean),
                    nameof(StdDev),
                });
    
        public override string ToString() =>
            string.Join(TextDelimiter.VerticalBarDelimiter.Key,
                new[]
                {
                    $"\"{CommandText.Replace("\"", "\"\"")}\"",
                    $"{CallCount}",
                    $"{ExceptionCount}",
                    $"{Min}",
                    $"{Max}",
                    $"{Mean}",
                    $"{StdDev}",
                });
    }
    
    public class DbInterceptorFixture
    {
        public static readonly DbInterceptorFixture Current = new();
        private bool _disposedValue;
        private ConcurrentDictionary<string, DatabaseCommandStatisticalData> DatabaseCommandData { get; } = new();
        private static IMasterLogger Logger { get; } = new MasterLogger(typeof(DbInterceptorFixture));
    
        /// <summary>
        /// Will run once at start up.
        /// </summary>
        private DbInterceptorFixture()
        {
            AssemblyLoadContext.Default.Unloading += Unloading;
        }
    
        /// <summary>
        /// A dummy method to call in order to ensure that static constructor is called
        /// at some more or less controlled time.
        /// </summary>
        public void Ping()
        {
        }
    
        public void LogDatabaseCall(DatabaseCommandInterceptorData data) =>
            DatabaseCommandData.AddOrUpdate(
                data.CommandText,
                _ => new DatabaseCommandStatisticalData(data.CommandText).Updated(data),
                (_, d) => d.Updated(data));
    
        private void Unloading(AssemblyLoadContext context)
        {
            if (_disposedValue) return;
            GC.SuppressFinalize(this);
            _disposedValue = true;
            SaveData();
        }
    
        private void SaveData()
        {
            try
            {
                File.WriteAllLines(
                    @"C:\Temp\Test.txt",
                    DatabaseCommandData
                        .Select(e => $"{e.Value}")
                        .Prepend(DatabaseCommandStatisticalData.Header));
            }
            catch (Exception e)
            {
                Logger.LogError(e);
            }
        }
    }
    

    然后在测试的某个地方注册一次DatabaseCommandInterceptor

    DbInterception.Add(new DatabaseCommandInterceptor());
    

    我也更喜欢在基础测试类中调用DbInterceptorFixture.Current.Ping(),尽管我不认为这是必要的。

    接口IMasterLogger 只是log4net 的强类型包装器,因此只需将其替换为您喜欢的接口即可。

    TextDelimiter.VerticalBarDelimiter.Key 的值就是'|',它位于我们所说的闭集内。

    PS 如果我搞砸了统计数据,请发表评论,我会更新答案。

    【讨论】:

    • 谁投反对票,请解释原因。 @jared-kells 解决方案很棒,因为它允许在不使用单个集合的情况下进行通用设置/拆卸。并且使用单个集合意味着所有这些可以并行运行的测试将连续运行。在我们的案例中,这将导致测试运行时间增加 10 倍或更多。但是,由于在 NET Core 应用程序关闭时不会调用终结器,因此他的解决方案在 NET Core 中不再适用,上面的代码显示了在这种情况下应该做什么。
    • 我怀疑你被否决了,因为你发布的 90% 以上的代码与问题无关(它对数据库没有任何要求),虽然你似乎提供了一些有用的见解(vis-a- .NET Core 中的终结者 - 我不知道),从你的回答中不清楚如何处理这些信息。
    【解决方案10】:

    只需使用静态构造函数,这就是你需要做的,它只运行一次。

    【讨论】:

      猜你喜欢
      • 2015-05-30
      • 2013-01-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-05-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多