[英]Run code once before and after ALL tests in xUnit.net
TL;DR - 我正在尋找與 MSTest 的AssemblyInitialize
等效的 xUnit(又名它具有我喜歡的一個功能)。
具體來說,我正在尋找它,因為我有一些 Selenium 煙霧測試,我希望能夠在沒有其他依賴項的情況下運行。 我有一個 Fixture 可以為我啟動 IisExpress 並在處置時終止它。 但是在每次測試之前都這樣做會大大增加運行時間。
我想在測試開始時觸發一次此代碼,並在結束時處理它(關閉進程)。 我怎么能 go 這樣做呢?
即使我可以通過編程方式訪問“當前正在運行多少個測試”之類的內容,我也可以弄清楚。
截至 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 )
注意:樣本取自文檔。
要在程序集初始化時執行代碼,則可以執行此操作(使用 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 創建一個 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
訪問夾具。
在今天的框架中是不可能做到的。 這是 2.0 計划的功能。
為了在 2.0 之前完成這項工作,您需要對框架進行重大的重新架構,或者編寫自己的運行程序來識別您自己的特殊屬性。
我使用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
}
}
我很惱火,因為沒有選擇在所有 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;
}
}
}
}
亮點:
在這里可以做更多的工作。 如果您想在不關心測試運行的情況下執行某些內容,您可以從 XunitTestFramework 繼承並包裝消息接收器。
您可以使用 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; }
}
編輯:修復新夾具為每個測試類創建的問題。
只需使用 static 構造函數,這就是您需要做的全部,它只運行一次。
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如果我搞砸了統計數據,請發表評論,我會更新答案。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.