Bereinigungs- und Anordnungspraktiken während des Integrationstests, um schmutzige Datenbanken zu vermeiden

9

Ich codiere Tests in C # und habe mich mit dieser Struktur abgefunden:

try
{
    // ==========
    // ARRANGE
    // ==========

    // Insert into the database all test data I'll need during the test

    // ==========
    // ACT
    // ==========

    // Do what needs to be tested

    // ==========
    // ASSERT
    // ==========

    // Check for correct behavior
}
finally
{
    // ==========
    // CLEANUP
    // ==========

    // Inverse of ARRANGE, delete the test data inserted during this test
}

Das Konzept war "jeder Test räumt das Chaos auf, das er macht". Bei einigen Tests bleibt die Datenbank jedoch verschmutzt, und die folgenden Tests schlagen fehl.

Was ist der richtige Weg, um dies zu tun? (Fehler minimieren, Laufzeit minimieren)

  • Deletes everything» Insert defaults» Insert test data»Test durchführen?
  • Insert defaults» Insert test data» Test ausführen » Delete everything?

  • Derzeit :

    • (pro Sitzung) Deletes everything»Insert defaults
    • (pro Test) Insert test data»Test ausführen»Delete test data
Dialex
quelle

Antworten:

7

Abgesehen von der Tatsache, dass dies ein Integrationstest im Gegensatz zu einem Komponententest ist, gehen die von Ihnen beschriebenen Operationen normalerweise in Setupund / oder TeardownMethoden ein. Mit Frameworks wie nUnit können Klassenmethoden mit diesen Attributen dekoriert werden, um anzugeben, ob es sich bei der Methode um eine Setup-Methode oder eine Teardown-Methode handelt.

Dann sollten Ihre Tests sauberer und kleiner werden, da die Einrichtung und Bereinigung außerhalb des Tests selbst erfolgt.

Höchstwahrscheinlich können mehrere Tests dieselben Daten wiederverwenden, was sowohl ein Plus als auch ein Einfügen / Entfernen bei jedem Test ist. Zurück zu nUnit : Die Attribute FixtureSetupund FixtureTeardownhelfen dabei, Daten für mehrere Tests gleichzeitig einzurichten .

Ich würde ein Testframework für einen Versuch / Fang verwenden, da viele dieser Testfunktionen in das Framework selbst integriert sind. xUnit, nUnit und sogar das in Microsoft integrierte Testframework sind eine gute Wahl und helfen bei der Einrichtung und Bereinigung von Datenbankdatensätzen auf konsistente Weise.

Jon Raynor
quelle
8

Der Punkt, den Sie mit solchen Tests anstreben sollten, ist, dass so viele von ihnen wie möglich mit einem Modell der Datenbank und nicht mit der Datenbank selbst interagieren sollten. Der Standardweg, um dies zu erreichen, besteht darin, eine DB-Zugriffsschicht über Schnittstellen in die hier getestete Logik einzufügen. Auf diese Weise kann der Testcode vor jedem Test speicherinterne Datensätze erstellen und diese anschließend in den Papierkorb verschieben. Die Tests können dann alle parallel ablaufen und beeinflussen sich nicht gegenseitig. Dies macht Ihre Tests schneller, einfacher zu schreiben und zu verstehen und robuster.

Anschließend müssen Sie die eigentliche DB-Zugriffsebene selbst testen. Da Sie nur über einige dieser Tests verfügen, können sie beispielsweise eine für diesen Test eindeutige Testtabelle (oder sogar Datenbank) erstellen und mit Testdaten füllen. Nachdem der Test ausgeführt wurde, wird die gesamte Testtabelle / DB zerstört. Auch diese Tests sollten parallel ausgeführt werden können und daher keinen wesentlichen Einfluss auf die Gesamtausführungszeit des Tests haben.

David Arno
quelle
Nun, das ist eine kleine Veränderung für uns im Moment (wir haben vor 3 Monaten mit Unit-Tests begonnen). Angenommen, wir verwenden vorerst eine echte Datenbank für Tests. Wie lautet die Standard- / Sicherheitsreihenfolge, um dies zu tun - alles löschen, alles einfügen und dann den Test ausführen?
Dialex
1
Wenn eine Umstrukturierung nicht in Frage kommt, bietet die Antwort von @ JonRaynor - in Ihrem Fall - die beste Option.
David Arno
5

Das große Problem bei Datenbanken und (Unit-) Tests ist, dass Datenbanken so verdammt gut darin sind, Dinge zu persistieren.

Die übliche Lösung besteht darin, in Ihren Komponententests keine tatsächliche Datenbank zu verwenden, sondern die Datenbank zu verspotten oder eine In-Memory-Datenbank zu verwenden, die zwischen den Tests problemlos vollständig gelöscht werden kann.
Nur beim Testen des Codes, der direkt mit der Datenbank interagiert, oder bei End-to-End-Tests wird die tatsächliche Datenbank verwendet.

Bart van Ingen Schenau
quelle
5

Auf einem C # -Server mit SQL Server und PetaPoco haben wir diesen Ansatz gewählt, um Daten in Unit-Tests zu bereinigen.

Ein typischer Unit-Test hätte Setup und Teardown wie folgt:

[TestFixture]
internal class PlatformDataObjectTests
{
    private IDatabaseConfiguration _dbConfig;
    private Database _pocoDatabase;
    private PlatformDataObject _platformDto;

    [SetUp]
    public void Setup()
    {
        _dbConfig = new CommonTestsAppConfig().GetDatabaseConfiguration();
        _pocoDatabase = new Database(_dbConfig.ConnectionString, SqlClientFactory.Instance);
        _platformDto = new PlatformDataObject(_pocoDatabase);
        _platformDto.BeginTransaction();
    }

    [TearDown]
    public void TearDown()
    {
        Console.WriteLine("Last Sql: {0}", _pocoDatabase.LastCommand);

        _platformDto.RollbackTransaction();
        _platformDto.Dispose();
    }

    // ... 
}

Wobei PlatformDataObject eine Klasse ist, die für die Kommunikation mit der Datenbank verantwortlich ist, z. B. für Select Insert Update Deletes. Alle * DataObject-Typen erben ServerDataObject - die Basisklasse verfügt über Methoden zum Abbrechen, Zurücksetzen oder Festschreiben der Transaktion.

/// <summary>
/// A Data-Transfer Object which allows creation and querying of Platform types from the database
/// </summary>
[ExportType(typeof(IPlatformDataObject))]
public class PlatformDataObject : ServerDataObject, IPlatformDataObject
{
    private static readonly ILog Log = LogManager.GetLogger(typeof (ProductDataObject));

    private const string PlatformTable = "t_Platform";

    public PlatformDataObject(IPocoDatabase pocoDatabase) : base(pocoDatabase)
    {
    }

    ... 
}

/// <summary>
/// A base Data-Transfer Object type
/// </summary>
public abstract class ServerDataObject : IServerDataObject
{
    protected const string Star = "*";

    private readonly IPocoDatabase _pocoDatabase;

    public ServerDataObject(IPocoDatabase pocoDatabase)
    {
        _pocoDatabase = pocoDatabase;
    }

    public string LastCommand
    {
        get { return PocoDatabase.LastCommand; }
    }

    public IPocoDatabase PocoDatabase
    {
        get { return _pocoDatabase; }
    }

    public int TransactionDepth
    {
        get { return _pocoDatabase.TransactionDepth; }
    }

    public bool TransactionAborted { get; private set; }

    public void BeginTransaction()
    {
        _pocoDatabase.BeginTransaction();
    }

    public void AbortTransaction()
    {
        _pocoDatabase.AbortTransaction();
    }

    public void RollbackTransaction()
    {
        TransactionAborted = true;
    }

    public virtual void Dispose()
    {
        if (TransactionAborted)
            _pocoDatabase.AbortTransaction();
        else
            _pocoDatabase.CompleteTransaction();
    }
}

Alle Komponententests würden RollbackTransaction () aufrufen und letztendlich IDbTransaction.Rollback () aufrufen.

In Tests haben wir festgestellt, dass es Routine ist, eine neue Instanz eines * DataObject zu erstellen, einige Zeilen mit Insert-Anweisungen zu erstellen, Tests für sie durchzuführen (Selects, Updates usw.) und dann ein Rollback durchzuführen.

Wir können eine Reihe von Testdaten einrichten, bevor alle Tests mit einem SetUpFixture ausgeführt werden - eine Klasse, die einmal ausgeführt wird, bevor alle Tests ausgeführt werden, und die Daten beim Herunterfahren löschen / zurücksetzen, nachdem alle Tests ausgeführt wurden.

Dr. Andrew Burnett-Thompson
quelle