136 votes

Exécuter le code une fois avant et après TOUS les tests dans xUnit.net

TL;DR - Je cherche l'équivalent pour xUnit de la fonction MSTest. AssemblyInitialize (c'est-à-dire la seule caractéristique que j'apprécie).

Plus précisément, je la recherche parce que j'ai quelques tests Selenium smoke que j'aimerais pouvoir exécuter sans autres dépendances. J'ai un Fixture qui lance IisExpress pour moi et le tue au moment de l'élimination. Mais faire cela avant chaque test gonfle énormément le temps d'exécution.

Je voudrais déclencher ce code une fois au début du test, et le détruire (en arrêtant le processus) à la fin. Comment puis-je m'y prendre ?

Même si je peux obtenir un accès programmatique à quelque chose comme "combien de tests sont en cours d'exécution", je peux trouver une solution.

4voto

J'étais assez ennuyé de ne pas avoir la possibilité d'exécuter des choses à la fin de tous les tests xUnit. Certaines des options proposées ici ne sont pas aussi intéressantes, car elles impliquent de modifier tous vos tests ou de les regrouper dans une seule collection (ce qui signifie qu'ils sont exécutés de manière synchrone). Mais la réponse de Rolf Kristensen m'a donné les informations nécessaires pour arriver à ce code. C'est un peu long, mais il suffit de l'ajouter à votre projet de test, sans avoir à modifier le code :

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

Les points forts :

  • montage : TestFramework indique à xUnit d'utiliser votre framework, qui proxies à celui par défaut
  • SideriteTestFramework englobe également l'exécuteur dans une classe personnalisée qui englobe ensuite le récepteur de messages
  • à la fin, la méthode Finished est exécutée, avec la liste des tests exécutés et le résultat du message xUnit

Un travail supplémentaire pourrait être effectué ici. Si vous voulez exécuter des choses sans vous soucier des tests exécutés, vous pourriez hériter de XunitTestFramework et simplement envelopper le récepteur de messages.

1voto

Khoa Le Points 41

Vous pouvez utiliser l'interface IUseFixture pour y parvenir. De plus, tous vos tests doivent hériter de la classe TestBase. Vous pouvez également utiliser OneTimeFixture directement depuis votre test.

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

EDIT : Correction du problème de la création d'un nouveau fixture pour chaque classe de test.

1voto

Movsar Bekaev Points 637

Il suffit d'utiliser le constructeur statique, c'est tout ce que vous devez faire, il ne s'exécute qu'une fois.

0voto

Vincent Points 707

Votre outil de construction offre-t-il une telle fonctionnalité ?

Dans le monde Java, lorsque l'on utilise Maven en tant qu'outil de construction, nous utilisons les les phases du cycle de vie de la construction . Par exemple, dans votre cas (tests d'acceptation avec des outils de type Selenium), on peut faire bon usage de l'option pre-integration-test y post-integration-test phases de démarrage et d'arrêt d'une application web avant et après l'intervention de l'utilisateur. integration-test s.

Je suis presque sûr que le même mécanisme peut être mis en place dans votre environnement.

0voto

La méthode décrite par Jared Kells ne fonctionne pas sous Net Core, parce que, eh bien, il n'est pas garanti que les finaliseurs seront appelés. Et, en fait, il n'est pas appelé pour le code ci-dessus. S'il vous plaît, voyez :

Pourquoi l'exemple Finalize/Destructor ne fonctionne-t-il pas dans .NET Core ?

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

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

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

Donc, sur la base de l'excellente réponse ci-dessus, voici ce que j'ai fini par faire (en remplaçant l'enregistrement dans un fichier si nécessaire) :

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

et ensuite enregistrer DatabaseCommandInterceptor une fois quelque part dans les tests :

DbInterception.Add(new DatabaseCommandInterceptor());

Je préfère aussi appeler DbInterceptorFixture.Current.Ping() dans la classe de test de base, bien que je ne pense pas que cela soit nécessaire.

L'interface IMasterLogger est juste une enveloppe fortement typée autour de log4net Il suffit donc de le remplacer par votre préféré.

La valeur de TextDelimiter.VerticalBarDelimiter.Key est juste '|' et il se trouve dans ce qu'on appelle un ensemble fermé.

PS : Si je me suis trompé dans les statistiques, veuillez commenter et je mettrai à jour la réponse.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X