99

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

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

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

即使我可以通过编程方式访问诸如“当前正在运行多少测试”之类的内容,我也可以弄清楚一些事情。

4

9 回答 9

78

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

基本上你需要创建一个做夹具的类:

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

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

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

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

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

注意:样本取自文档

于 2015-11-18T11:05:50.577 回答
38

要在程序集初始化时执行代码,可以这样做(使用 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

于 2018-11-04T17:23:20.123 回答
29

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

您可以使用 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

于 2013-02-19T06:03:42.703 回答
16

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

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

于 2012-12-19T15:41:38.257 回答
3

我使用AssemblyFixture ( NuGet )。

它的作用是提供一个IAssemblyFixture<T>接口,该接口替换IClassFixture<T>您希望对象生命周期作为测试程序集的任何位置。

例子:

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  
  }
}
于 2017-06-14T02:00:14.253 回答
3

我对在所有 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 继承并只包装消息接收器。

于 2019-10-14T21:07:26.930 回答
1

您可以使用 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; }
}

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

于 2015-02-01T12:13:26.460 回答
0

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

在 Java 世界中,当使用Maven作为构建工具时,我们会使用构建生命周期的适当阶段。例如,在您的情况下(使用类似 Selenium 的工具进行验收测试),可以充分利用pre-integration-test和阶段在一个人的spost-integration-test之前/之后启动/停止 webapp 。integration-test

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

于 2012-12-19T17:50:24.057 回答
0

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

为什么 Finalize/Destructor 示例在 .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如果我搞砸了统计数据,请发表评论,我会更新答案。

于 2021-02-24T00:35:26.140 回答