Freigeben von Datenbanken zwischen TestsSharing databases between tests

Im Beispiel für die EF Core Tests wurde gezeigt, wie Sie Anwendungen mit verschiedenen Datenbanksystemen testen.The EF Core testing sample showed how to test applications against different database systems. In diesem Beispiel hat jeder Test eine neue Datenbank erstellt.For that sample, each test created a new database. Dies ist ein gutes Muster bei der Verwendung von SQLite oder der EF-in-Memory Database, aber es kann einen erheblichen mehr Aufwand bei der Verwendung anderer Datenbanksysteme bedeuten.This is a good pattern when using SQLite or the EF in-memory database, but it can involve significant overhead when using other database systems.

Dieses Beispiel baut auf dem vorherigen Beispiel auf, indem die Daten Bank Erstellung in eine Test Fixierung verschoben wird.This sample builds on the previous sample by moving database creation into a test fixture. Dadurch kann eine einzelne SQL Server Datenbank erstellt und nur einmal für alle Tests durchsucht werden.This allows a single SQL Server database to be created and seeded only once for all tests.

Tipp

Stellen Sie sicher, dass Sie das Beispiel für EF Core Tests durcharbeiten, bevor Sie fortfahren.Make sure to work through the EF Core testing sample before continuing here.

Es ist nicht schwierig, mehrere Tests für dieselbe Datenbank zu schreiben.It's not difficult to write multiple tests against the same database. Der Trick besteht darin, dass die Tests nicht aufeinander verlaufen, während Sie ausgeführt werden.The trick is doing it in a way that the tests don't trip over each other as they run. Dies erfordert Folgendes:This requires understanding:

  • Sicheres Freigeben von Objekten zwischen TestsHow to safely share objects between tests
  • Wenn das Test Framework Tests parallel ausführtWhen the test framework runs tests in parallel
  • So halten Sie die Datenbank für jeden Test in einem sauberen ZustandHow to keep the database in a clean state for every test

Die FixierungThe fixture

Wir verwenden eine Test Fixierung für die Freigabe von Objekten zwischen Tests.We will use a test fixture for sharing objects between tests. In der xUnit-Dokumentation wird angegeben, dass eine Fixierung verwendet werden soll: "Wenn Sie einen einzelnen Test Kontext erstellen und für alle Tests in der Klasse freigeben möchten, und nachdem alle Tests in der Klasse abgeschlossen wurden."The XUnit documentation states that a fixture should be used "when you want to create a single test context and share it among all the tests in the class, and have it cleaned up after all the tests in the class have finished."

Tipp

In diesem Beispiel wird xUnitverwendet, aber ähnliche Konzepte sind in anderen Test-Frameworks, einschließlich nunit, vorhanden.This sample uses XUnit, but similar concepts exist in other testing frameworks, including NUnit.

Dies bedeutet, dass wir die Daten Bank Erstellung und das Seeding in eine Fixierungs Klasse verschieben müssen.This means that we need to move database creation and seeding to a fixture class. Das Fenster sieht so aus:Here's what it looks like:

public class SharedDatabaseFixture : IDisposable
{
    private static readonly object _lock = new object();
    private static bool _databaseInitialized;

    public SharedDatabaseFixture()
    {
        Connection = new SqlConnection(@"Server=(localdb)\mssqllocaldb;Database=EFTestSample;ConnectRetryCount=0");

        Seed();

        Connection.Open();
    }

    public DbConnection Connection { get; }

    public ItemsContext CreateContext(DbTransaction transaction = null)
    {
        var context = new ItemsContext(new DbContextOptionsBuilder<ItemsContext>().UseSqlServer(Connection).Options);

        if (transaction != null)
        {
            context.Database.UseTransaction(transaction);
        }

        return context;
    }

    private void Seed()
    {
        lock (_lock)
        {
            if (!_databaseInitialized)
            {
                using (var context = CreateContext())
                {
                    context.Database.EnsureDeleted();
                    context.Database.EnsureCreated();

                    var one = new Item("ItemOne");
                    one.AddTag("Tag11");
                    one.AddTag("Tag12");
                    one.AddTag("Tag13");

                    var two = new Item("ItemTwo");

                    var three = new Item("ItemThree");
                    three.AddTag("Tag31");
                    three.AddTag("Tag31");
                    three.AddTag("Tag31");
                    three.AddTag("Tag32");
                    three.AddTag("Tag32");

                    context.AddRange(one, two, three);

                    context.SaveChanges();
                }

                _databaseInitialized = true;
            }
        }
    }

    public void Dispose() => Connection.Dispose();
}

Beachten Sie vorerst, wie der Konstruktor lautet:For now, notice how the constructor:

  • Erstellt eine einzelne Datenbankverbindung für die Lebensdauer der Fixierung.Creates a single database connection for the lifetime of the fixture
  • Erstellt und erstellt die Datenbank durch Aufrufen der- Seed Methode.Creates and seeds that database by calling the Seed method

Sperre vorerst ignorieren; Wir werden später darauf zurückkommen.Ignore the locking for now; we will come back to it later.

Tipp

Der Erstellungs-und Seeding Code muss nicht Async sein.The creation and seeding code does not need to be async. Die asynchrone Erstellung erschwert den Code und führt nicht zu einer Verbesserung der Leistung oder des Durchsatzes von Tests.Making it async will complicate the code and will not improve performance or throughput of tests.

Die Datenbank wird erstellt, indem zuerst eine vorhandene Datenbank gelöscht und dann eine neue Datenbank erstellt wird.The database is created by first deleting any existing database and then creating a new database. Dadurch wird sichergestellt, dass die Datenbank mit dem aktuellen EF-Modell übereinstimmt, auch wenn Sie seit dem letzten Testlauf geändert wurde.This ensures that the database matches the current EF model even if it has been changed since the last test run.

Tipp

Es kann schneller sein, die vorhandene Datenbank zu "bereinigen", indem Sie etwas ähnliches wie das Erstellen von Vorgängen verwendet, anstatt Sie jedes Mal neu zu erstellen.It can be faster to "clean" the existing database using something like respawn rather than re-create it each time. Es muss jedoch darauf geachtet werden, dass das Datenbankschema mit dem EF-Modell auf dem neuesten Stand ist.However, care must be taken to ensure that the database schema is up-to-date with the EF model when doing this.

Die Datenbankverbindung wird verworfen, wenn die Fixierung verworfen wird.The database connection is disposed when the fixture is disposed. Sie können auch das Löschen der Testdatenbank an dieser Stelle in Erwägung gezogen.You may also consider deleting the test database at this point. Dies erfordert jedoch zusätzliche Sperren und eine Verweis Zählung, wenn die Fixierung von mehreren Test Klassen gemeinsam genutzt wird.However, this will require additional locking and reference counting if the fixture is being shared by multiple test classes. Außerdem ist es häufig hilfreich, dass die Testdatenbank weiterhin zum Debuggen von fehlgeschlagenen Tests verfügbar ist.Also, it is often useful to have the test database still available for debugging failed tests.

Verwenden der FixierungUsing the fixture

XUnit hat ein gängiges Muster zum Zuordnen einer Test Fixierung zu einer Klasse von Tests:XUnit has a common pattern for associating a test fixture with a class of tests:

public class SharedDatabaseTest : IClassFixture<SharedDatabaseFixture>
{
    public SharedDatabaseTest(SharedDatabaseFixture fixture) => Fixture = fixture;

    public SharedDatabaseFixture Fixture { get; }

XUnit erstellt jetzt eine einzelne Fixierungs Instanz und übergibt sie an jede Instanz der Testklasse.XUnit will now create a single fixture instance and pass it to each instance of the test class. (Beachten Sie das erste Testbeispiel , dass xUnit bei jeder Ausführung eines Tests eine neue Test Klasseninstanz erstellt.) Dies bedeutet, dass die Datenbank einmal erstellt und ein Seeding erstellt wird, und dann wird jeder Test diese Datenbank verwenden.(Remember from the first testing sample that XUnit creates a new test class instance every time it runs a test.) This means that the database will be created and seeded once and then each test will use this database.

Beachten Sie, dass Tests in einer einzelnen Klasse nicht parallel ausgeführt werden.Note that tests within a single class will not be run in parallel. Dies bedeutet, dass es für jeden Test sicher ist, dieselbe Datenbankverbindung zu verwenden, auch wenn das DbConnection Objekt nicht Thread sicher ist.This means it is safe for each test to use the same database connection, even though the DbConnection object is not thread-safe.

Verwalten des Daten Bank StatusMaintaining database state

Tests müssen häufig die Testdaten durch Einfügungen, Updates und Löschungen mutieren.Tests often need to mutate the test data with inserts, updates, and deletes. Diese Änderungen wirken sich jedoch auf andere Tests aus, bei denen eine saubere, Seeding Datenbank erwartet wird.But these changes will then impact other tests which are expecting a clean, seeded database.

Dies kann durch Ausführen von veränderenden Tests innerhalb einer Transaktion behoben werden.This can be dealt with by running mutating tests inside a transaction. Beispiel:For example:

[Fact]
public void Can_add_item()
{
    using (var transaction = Fixture.Connection.BeginTransaction())
    {
        using (var context = Fixture.CreateContext(transaction))
        {
            var controller = new ItemsController(context);

            var item = controller.PostItem("ItemFour").Value;

            Assert.Equal("ItemFour", item.Name);
        }

        using (var context = Fixture.CreateContext(transaction))
        {
            var item = context.Set<Item>().Single(e => e.Name == "ItemFour");

            Assert.Equal("ItemFour", item.Name);
            Assert.Equal(0, item.Tags.Count);
        }
    }
}

Beachten Sie, dass die Transaktion beim Start des Tests erstellt und verworfen wird.Notice that the transaction is created as the test starts and disposed when it is finished. Das Verwerfen der Transaktion bewirkt, dass ein Rollback ausgeführt wird, sodass keine der Änderungen von anderen Tests erkannt wird.Disposing the transaction causes it to be rolled back, so none of the changes will be seen by other tests.

Die Hilfsmethode zum Erstellen eines Kontexts (siehe den obigen Fixierungs Code) akzeptiert diese Transaktion und optet den dbcontext in der Verwendung.The helper method for creating a context (see the fixture code above) accepts this transaction and opts the DbContext into using it.

Freigeben der FixierungSharing the fixture

Sie haben vielleicht bemerkt, dass Sie den Code für die Erstellung und das Seeding der DatenbankYou may have noticed locking code around database creation and seeding. Dies ist für dieses Beispiel nicht erforderlich, da nur eine Klasse von Tests die Fixierung verwendet, sodass nur eine einzige Fixierungs Instanz erstellt wird.This is not needed for this sample since only one class of tests use the fixture, so only a single fixture instance is created.

Möglicherweise möchten Sie jedoch dieselbe Fixierung mit mehreren Test Klassen verwenden.However, you may want to use the same fixture with multiple classes of tests. XUnit erstellt eine Fixierungs Instanz für jede dieser Klassen.XUnit will create one fixture instance for each of these classes. Diese können von verschiedenen Threads verwendet werden, die Tests parallel ausführen.These may be used by different threads running tests in parallel. Daher ist es wichtig, dass Sie über eine geeignete Sperre verfügen, um sicherzustellen, dass nur ein Thread die Erstellung und das Seeding der Datenbank übernimmtTherefore, it is important to have appropriate locking to ensure only one thread does the database creation and seeding.

Tipp

Eine einfache lock ist in Ordnung.A simple lock is fine here. Es ist nicht erforderlich, etwas komplexeres zu versuchen, wie z. b. sperrfreie Muster.There is no need to attempt anything more complex, such as any lock-free patterns.