Führen Sie den Code einmal vor und nach ALLEN Tests in xUnit.net aus

78

TL; DR - Ich suche nach xUnits Äquivalent zu MSTests AssemblyInitialize(auch bekannt als die ONE-Funktion, die mir gefällt).

Insbesondere suche ich danach, weil ich einige Selen-Rauchtests habe, die ich ohne andere Abhängigkeiten ausführen möchte. Ich habe ein Fixture, das IisExpress für mich startet und es bei der Entsorgung tötet. Aber dies zu tun, bevor jeder Test die Laufzeit enorm aufbläht.

Ich möchte diesen Code zu Beginn des Tests einmal auslösen und am Ende entsorgen (den Prozess herunterfahren). Wie könnte ich das machen?

Selbst wenn ich programmgesteuerten Zugriff auf etwas wie "Wie viele Tests werden derzeit ausgeführt" erhalten kann, kann ich etwas herausfinden.

George Mauer
quelle
Meinen Sie xUnit als "die generische Gruppe sprachspezifischer Unit-Test-Tools wie JUnit, NUnit usw." oder xUnit als "xUnit.net, das .Net Unit Testing Tool"?
Andy Tinkham
3
Basierend auf dieser Tabelle xunit.codeplex.com/… glaube ich nicht, dass es ein Äquivalent gibt. Eine Problemumgehung besteht darin, Ihre Assembly-Initialisierung in einen Singleton zu verschieben und von jedem Ihrer Konstruktoren aufzurufen.
Allen
@allen - Das ist ähnlich wie das, was ich mache, aber es gibt mir einen Assembly-Initialisierer ohne einen Assembly-Teardown. Deshalb habe ich nach der Testanzahl gefragt.
George Mauer

Antworten:

57

Ab November 2015 ist xUnit 2 verfügbar, daher gibt es eine kanonische Möglichkeit, Funktionen zwischen Tests auszutauschen. Es ist hier dokumentiert .

Grundsätzlich müssen Sie eine Klasse erstellen, die das Fixture ausführt:

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

Eine Dummy-Klasse mit dem CollectionDefinitionAttribut. Diese Klasse ermöglicht es Xunit, eine Testsammlung zu erstellen, und verwendet das angegebene Gerät für alle Testklassen der Sammlung.

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

Dann müssen Sie den Sammlungsnamen über alle Ihre Testklassen hinzufügen. Die Testklassen können das Gerät über den Konstruktor empfangen.

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

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

Es ist etwas ausführlicher als MsTests, AssemblyInitializeda Sie für jede Testklasse angeben müssen, zu welcher Testsammlung sie gehört, aber es ist auch modulierbarer (und mit MsTests müssen Sie Ihren Klassen noch eine Testklasse hinzufügen).

Hinweis: Die Proben wurden der Dokumentation entnommen .

gwenzek
quelle
10
Wenn ich Folgendes lese: "... // Diese Klasse hat keinen Code und wird nie erstellt ...", dann bevorzuge ich wirklich die Microsoft-Implementierung einer AssemblyInitialize. Viel eleganter.
Elisabeth
Ich bin damit einverstanden, dass es ein bisschen komisch ist
Gwenzek
8
@ Elizabeth Ich habe dies gerade verwendet und sowohl das [CollectionDefinition("Database collection")]Attribut als auch die ICollectionFixture<DatabaseFixture>Schnittstelle zur DatabaseFixtureKlasse hinzugefügt und alles funktioniert. Es entfernt eine leere Klasse und scheint mir sauberer!
Christian Droulers
11
Das Attribut [Sammlung] verhindert, dass Tests parallel ausgeführt werden. Gibt es einen anderen Ansatz für die globale Initialisierung / den globalen Abbau in xUnit, mit dem Tests parallel ausgeführt werden können?
IT Hit WebDAV
1
Ich weiß, dass dies alt ist, aber wie haben Sie mehrere Sammlungen in einer einzelnen Testklasse? Ich möchte, dass der Datenbank-Setup-Code sowie mein Zuordnungscode ausgeführt werden.
Ball
24

Erstellen Sie ein statisches Feld und implementieren Sie einen Finalizer.

Sie können die Tatsache nutzen, dass xUnit eine AppDomain erstellt, um Ihre Testassembly auszuführen und sie nach Abschluss zu entladen. Durch das Entladen der App-Domäne wird der Finalizer ausgeführt.

Ich verwende diese Methode, um IISExpress zu starten und zu stoppen.

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

Bearbeiten: Greifen Sie ExampleFixture.Currentin Ihren Tests auf das Gerät zu .

Jared Kells
quelle
Interessant - wird die Appdomain sofort entsorgt oder dauert es eine Weile? Mit anderen Worten, was ist, wenn ich zwei Testläufe hintereinander habe?
George Mauer
Die AppDomain wird sofort nach Abschluss des letzten Tests in Ihrer Assembly entladen. Wenn in derselben Testbaugruppe 100 Tests hintereinander ausgeführt werden, werden diese nach Abschluss des 100. Tests entladen.
Jared Kells
1
Ich verwende diese Methode, um IISExpress zu Beginn meiner Tests zu starten und zu stoppen, nachdem alle abgeschlossen sind. Es funktioniert gut in ReSharper und MSBuild in Teamcity.
Jared Kells
1
@ GeorgeMauer und Jared, vielleicht AppDomain Unloadkönnte die Veranstaltung nützlicher sein? (Natürlich sind während des Herunterfahrens alle Wetten aus, aber es kann nur ein bisschen zuverlässiger aus dem Speicher sein)
Ruben Bartelink
2
Dies ist nur anekdotisch, aber seit ich diese Antwort gepostet habe, ist mein Build-Server einen Monat lang gelaufen und hat 300 Builds mit dieser Methode durchgeführt, und es funktioniert einwandfrei.
Jared Kells
21

Um Code bei der Assembly-Initialisierung auszuführen, kann dies ausgeführt werden (Getestet mit 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();
      }
   }
}

Siehe auch https://github.com/xunit/samples.xunit/tree/master/AssemblyFixtureExample

Rolf Kristensen
quelle
Hallo, das sieht interessant aus: Können wir mehr Informationen haben?
Jonatha ANTOINE
1
@ JonathaANTOINE Denken Sie, Sie sollten eine neue Stackoverflow-Frage erstellen, anstatt mit einer offenen Frage mit 10000 möglichen Antworten zu beginnen.
Rolf Kristensen
ok, ich habe herausgefunden, wie das geht und ich habe einen Blog-Beitrag eingegangen :)
Jonatha ANTOINE
16

Dies ist heute im Rahmen nicht möglich. Dies ist eine für 2.0 geplante Funktion.

Damit dies vor 2.0 funktioniert, müssen Sie eine erhebliche Neuarchitektur des Frameworks durchführen oder Ihre eigenen Läufer schreiben, die Ihre eigenen speziellen Attribute erkennen.

Brad Wilson
quelle
Danke Brad, solange ich dich hier habe, hast du eine Idee zu meiner letzten Frage zum VS-Testläufer? visualstudiogallery.msdn.microsoft.com/…
George Mauer
1
Hey Brad - sieh dir die Antwort von @JaredKells unten an. Siehst du irgendwelche Probleme mit diesem Ansatz?
George Mauer
1
Laut dem .NET-Framework gibt es keine Garantie dafür, wann ein Finalizer ausgeführt wird oder ob er überhaupt ausgeführt wird. Wenn Sie damit vertraut sind, dann ist sein Vorschlag wohl in Ordnung. :)
Brad Wilson
5
Nachdem 2.0 veröffentlicht wurde, besteht die Lösung, bei der Sie KEINE gemeinsame Basisklasse für alle Ihre Tests benötigen (oder auf andere Weise einen gemeinsamen Code aufrufen), darin, Ihre eigene Unterklasse von XunitTestFramework zu implementieren und Ihre Initialisierung im Konstruktor durchzuführen und Finalizer und markieren Sie die Assembly mit einem TestFrameworkAttribute. Eine Alternative zum Finalizer besteht darin, der geschützten DisposalTracker-Eigenschaft der Basisklasse TestFramework, die die Bereinigung durchführt, ein Objekt hinzuzufügen, wenn Sie sich damit nicht wohl fühlen.
Avi Cherry
1
@AviCherry, das sollte jetzt die Antwort auf die ursprüngliche Frage sein.
Hat super
3

Ich benutze AssemblyFixture ( NuGet ).

Es bietet eine IAssemblyFixture<T>Schnittstelle, die alle ersetzt, an IClassFixture<T>denen die Lebensdauer des Objekts als Testbaugruppe erfolgen soll.

Beispiel:

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  
  }
}
Shimmy Weitzhandler
quelle
Korrigieren Sie mich, wenn ich falsch liege, aber es ist erwähnenswert, dass dies xUnit 2+ ist
George Mauer
Es funktioniert nur, wenn Sie [assembly: TestFramework("Xunit.Extensions.Ordering.TestFramework", "Xunit.Extensions.Ordering")]einer Klasse des Projekts hinzufügen . Referenz
Manjeet Lama
2

Ich war ziemlich verärgert darüber, dass ich am Ende aller xUnit-Tests nicht die Möglichkeit hatte, Dinge auszuführen. Einige der hier aufgeführten Optionen sind nicht so großartig, da sie alle Ihre Tests ändern oder unter eine Sammlung stellen (was bedeutet, dass sie synchron ausgeführt werden). Aber die Antwort von Rolf Kristensen gab mir die notwendigen Informationen, um an diesen Code zu gelangen. Es ist etwas lang, aber Sie müssen es nur zu Ihrem Testprojekt hinzufügen, es sind keine weiteren Codeänderungen erforderlich:

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

Die Höhepunkte:

  • Assembly: TestFramework weist xUnit an, Ihr Framework zu verwenden, das dem Standard-Framework entspricht
  • SideriteTestFramework verpackt den Executor auch in eine benutzerdefinierte Klasse, die dann die Nachrichtensenke umschließt
  • Am Ende wird die Methode "Fertig" mit der Liste der ausgeführten Tests und dem Ergebnis der xUnit-Nachricht ausgeführt

Hier könnte noch mehr Arbeit geleistet werden. Wenn Sie Dinge ausführen möchten, ohne sich um den Testlauf zu kümmern, können Sie von XunitTestFramework erben und einfach die Nachrichtensenke einpacken.

Siderit Zackwehdex
quelle
1

Sie können die IUseFixture-Schnittstelle verwenden, um dies zu erreichen. Außerdem muss Ihr gesamter Test die TestBase-Klasse erben. Sie können OneTimeFixture auch direkt aus Ihrem Test heraus verwenden.

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

BEARBEITEN: Beheben Sie das Problem, das neue Geräte für jede Testklasse verursachen.

Khoa Le
quelle
Dies ist sowohl ein falscher als auch ein schlechter Rat. Die Methoden von ApplicationFixturewerden vor und nach jedem Test ausgeführt, nicht nur einmal. Es ist auch ein schlechter Rat, immer von demselben zu erben, der TestBaseIhren einen Vererbungslink verbraucht, und Sie können ihn nicht mehr verwenden, um gemeinsame Methoden zwischen einer Gruppe verwandter Klassen zu teilen. Genau deshalb IUseFixturewurde es erfunden, um sich nicht auf Vererbung verlassen zu müssen. Schließlich werden Sie feststellen, dass der Ersteller von xUnit bereits die akzeptierte Antwort auf die Frage hat, dass dies bis zum Versand von 2.0 nicht ordnungsgemäß möglich ist.
George Mauer
Naja eigentlich läuft es nicht vor jedem Test. Wenn Sie IUseFixture verwenden, erstellt das Fixture nur einmal für jeden Testklassentyp. Wenn Sie also Ihren Code in den Fixture-Konstruktor einfügen, wird er nur einmal ausgeführt. Das Schlimme ist, dass es für jede Art von Testklasse einmal ausgeführt wird, und das weiß ich auch. Ich dachte, das wird eine Instanz für jede Testsitzung erstellen, aber das ist es nicht. Ich ändere nur den Beispielcode, um dieses Problem zu lösen. Verwenden Sie eine statische Variable, um die Fixture-Instanz zu speichern, um sicherzustellen, dass sie nur einmal erstellt wird, und ein AppDomain Unload-Ereignis, um Fixture zu entsorgen.
Khoa Le
0

Bietet Ihr Build-Tool eine solche Funktion?

In der Java-Welt verwenden wir bei Verwendung von Maven als Build-Tool die entsprechenden Phasen des Build-Lebenszyklus . In Ihrem Fall (Abnahmetests mit Selen-ähnlichen Tools) kann man beispielsweise die Phasen pre-integration-testund gut nutzen, post-integration-testum eine Webanwendung vor / nach der eigenen zu starten / zu stoppen integration-test.

Ich bin mir ziemlich sicher, dass der gleiche Mechanismus in Ihrer Umgebung eingerichtet werden kann.

Vincent
quelle
Mit einem vollwertigen Build-System ist natürlich alles möglich. Ich kann das ziemlich einfach mit psake oder grunzen einrichten. Das Problem ist, dass in Visual Studio integrierte Testläufer Build-Systeme nicht einfach zum Ausführen ihrer Tests verwenden. Soweit ich von ihren Codebasen gesehen habe, werden sie direkt von der IDE aufgerufen und führen selbst alle DLLs direkt aus.
George Mauer