99

AssemblyInitializeTL;DR - xUnit の MSTest に相当するもの(つまり、私が気に入っている 1 つの機能)を探しています。

具体的には、他の依存関係なしで実行できるようにしたいいくつかの Selenium スモーク テストがあるため、それを探しています。私のために IisExpress を起動し、廃棄時にそれを強制終了する Fixture があります。しかし、すべてのテストの前にこれを行うと、ランタイムが大幅に肥大化します。

テストの開始時にこのコードを 1 回トリガーし、最後に破棄 (プロセスをシャットダウン) したいと思います。どうすればそれを行うことができますか?

「現在実行中のテストの数」などにプログラムでアクセスできたとしても、何かを理解できます。

4

9 に答える 9

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 に答える
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を使用する場合、ビルド ライフサイクルの適切なフェーズを使用します。pre-integration-testたとえば、あなたの場合 (Selenium のようなツールを使用した受け入れテスト)、とフェーズをうまく利用して、 のpost-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 に答える