在 xUnit.net 中的所有测试之前和之后运行一次代码

DR-我正在寻找 xUnit 的相当于 MSTest 的 AssemblyInitialize(也就是我喜欢的 ONE 特性)。

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

I would like to trigger this code once at the start of testing, and dispose of it (shutting down the process) at the end. How could I go about doing that?

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

87687 次浏览

现在在框架中是不可能做到的,这是计划在2.0版本中实现的特性。

为了在2.0之前实现这个目标,需要对框架进行重大的重构,或者编写能够识别自己特殊属性的跑步者。

Does your build tool provide such a feature?

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

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

Create a static field and implement a finalizer.

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

I am using this method to start and stop 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访问夹具。

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

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

As of Nov 2015 xUnit 2 is out, so there is a canonical way to share features between tests. It is documented 给你.

基本上,您需要创建一个类来完成 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 创建一个测试集合,并且将为集合的所有测试类使用给定的 fixture。

    [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;
}
}

它比 MsTestAssemblyInitialize稍微冗长一些,因为您必须在每个测试类上声明它所属于的测试集合,但是它也更具有可模块性(而且在使用 MsTest 时,您仍然需要在类上放置 TestClass)

注: 样品取自 文件

我使用 装配装置(NuGet)。

What it does is it provides an IAssemblyFixture<T> interface that is replacing any IClassFixture<T> where you want the object's lifetime to be as the testing assembly.

例如:

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

在所有 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;
}
}
}
}

重点:

  • assembly: TestFramework instructs xUnit to use your framework, which 默认值的代理
  • SideriteTestFramework also wraps the executor into a custom class 然后包装消息接收器
  • 最后,使用测试列表执行 Finish 方法 run and the result from the xUnit message

这里可以做更多的工作。如果您希望执行某些内容而不关心测试运行,那么您可以从 XunitTestFramework 继承,并只包装消息接收器。

贾里德 · 凯尔斯描述的方法 does not work under Net Core, because, well it is not guaranteed that finalizers will be called. And, in fact, it is not called for the code above. Please, see:

为什么 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 如果我在统计方面搞砸了,请发表评论,我会更新答案。

Just use the static constructor, that's all you need to do, it runs just once.