wake-up-neo.net

Wie kann ich Komponententests in großen und komplexen Klassen implementieren?

Ich führe Unit-Tests in einem Finanzsystem durch, das mehrere Berechnungen umfasst. Eine der Methoden empfängt ein Objekt per Parameter mit mehr als 100 Eigenschaften und berechnet basierend auf den Eigenschaften dieses Objekts die Rendite. Um Unit-Tests für diese Methode durchzuführen, muss das gesamte Objekt mit gültigen Werten gefüllt sein.

Also ... Frage: Heute wird dieses Objekt über die Datenbank gefüllt. Bei meinen Unit-Tests (ich verwende NUnit) müsste ich die Datenbank meiden und ein Mock-Objekt davon erstellen, um nur die Rückgabe der Methode zu testen. Wie kann ich diese Methode mit diesem riesigen Objekt effizient testen? Muss ich wirklich alle 100 Eigenschaften manuell ausfüllen? Gibt es eine Möglichkeit, diese Objekterstellung mit Moq (zum Beispiel) zu automatisieren?

obs: Ich schreibe Unit-Tests für ein System, das bereits erstellt wurde. Es ist derzeit nicht möglich, die gesamte Architektur neu zu schreiben.
Tausend Dank!

17
Lisa Shiphrah

Wenn diese 100 Werte nicht relevant sind und Sie nur einige davon benötigen, haben Sie mehrere Optionen.

Sie können ein neues Objekt erstellen (Eigenschaften werden mit Standardwerten wie null für Zeichenfolgen und 0 für Ganzzahlen initialisiert) und weisen nur die erforderlichen Eigenschaften zu:

 var obj = new HugeObject();
 obj.Foo = 42;
 obj.Bar = "banana";

Sie können auch eine Bibliothek wie AutoFixture verwenden, die Dummy-Werte für alle Eigenschaften in Ihrem Objekt zuweist:

 var fixture = new Fixture();
 var obj = fixture.Create<HugeObject>();

Sie können die erforderlichen Eigenschaften manuell zuweisen oder den Geräte-Builder verwenden

 var obj = fixture.Build<HugeObject>()
                  .With(o => o.Foo, 42)
                  .With(o => o.Bar, "banana")
                  .Create();

Eine andere nützliche Bibliothek für denselben Zweck ist NBuilder


HINWEIS: Wenn alle Eigenschaften für das von Ihnen getestete Feature relevant sind und bestimmte Werte haben sollten, gibt es keine Bibliothek, die die für den Test erforderlichen Werte erraten wird. Die einzige Möglichkeit besteht darin, Testwerte manuell festzulegen. Sie können jedoch eine Menge Arbeit vermeiden, wenn Sie vor jedem Test einige Standardwerte festlegen und einfach die für einen bestimmten Test benötigten Informationen ändern. Das heißt Hilfsmethode (n) erstellen, die ein Objekt mit vordefinierten Werten erstellt:

 private HugeObject CreateValidInvoice()
 {
     return new HugeObject {
         Foo = 42,
         Bar = "banaba",
         //...
     };
 }

Und dann überschreiben Sie in Ihrem Test einfach einige Felder:

 var obj = CreateValidInvoice();
 obj.Bar = "Apple";
 // ...
13

Für den Fall, dass ich eine große Menge an korrekten Daten zum Testen abrufen musste, habe ich die Daten in JSON serialisiert und direkt in meine Testklassen aufgenommen. Originaldaten können aus Ihrer Datenbank entnommen und anschließend serialisiert werden. Etwas wie das:

[Test]
public void MyTest()
{
    // Arrange
    var data = GetData();

    // Act
    ... test your stuff

    // Assert
    .. verify your results
}


public MyBigViewModel GetData()
{
    return JsonConvert.DeserializeObject<MyBigViewModel>(Data);
}

public const String Data = @"
{
    'SelectedOcc': [29, 26, 27, 2,  1,  28],
    'PossibleOcc': null,
    'SelectedCat': [6,  2,  5,  7,  4,  1,  3,  8],
    'PossibleCat': null,
    'ModelName': 'c',
    'ColumnsHeader': 'Header',
    'RowsHeader': 'Rows'
    // etc. etc.
}";

Dies ist möglicherweise nicht optimal, wenn Sie viele Tests wie diese haben, da es ziemlich lange dauert, die Daten in diesem Format abzurufen. Dadurch erhalten Sie jedoch Basisdaten, die Sie nach Abschluss der Serialisierung für verschiedene Tests ändern können.

Um diese JSON zu erhalten, müssen Sie die Datenbank separat nach diesem großen Objekt abfragen, sie mit JsonConvert.Serialise in JSON serialisieren und diesen String in Ihren Quellcode aufnehmen. Dieses Bit ist relativ einfach, erfordert jedoch einige Zeit, da Sie es manuell tun müssen ... aber nur einmal.

Ich habe diese Technik erfolgreich angewendet, als ich das Rendern von Berichten testen musste und Daten aus der Datenbank abrufen mussten, war für den aktuellen Test kein Thema.

p.s. Sie benötigen ein Newtonsoft.Json-Paket, um JsonConvert.DeserializeObject zu verwenden.

5
trailmax

In Anbetracht der Einschränkungen (schlechtes Code-Design und technische Verschuldung ... ich scherze) Ein Unit-Test ist sehr umständlich, manuell zu füllen. Ein hybrider Integrationstest wäre dann erforderlich, wenn Sie eine tatsächliche Datenquelle treffen müssen (nicht die in der Produktion befindliche). 

Mögliche Tränke

  1. Erstellen Sie eine Kopie der Datenbank und füllen Sie nur die Tabellen/Daten aus, die zum Auffüllen der abhängigen komplexen Klasse erforderlich sind. Hoffentlich ist der Code so modularisiert, dass der Datenzugriff die komplexe Klasse erhalten und füllen kann. 

  2. Mock den Datenzugriff und lassen Sie die notwendigen Daten über eine alternative Quelle importieren (Flatfile vielleicht? Csv)

Der gesamte andere Code könnte sich auf das Verspotten aller anderen Abhängigkeiten konzentrieren, die zur Durchführung des Komponententests erforderlich sind.

Die einzige andere Option besteht darin, die Klasse manuell auszufüllen.

Abgesehen davon hat dies einen schlechten Code-Geruch, der jedoch außerhalb des Geltungsbereichs des OP liegt, da er momentan nicht geändert werden kann. Ich würde vorschlagen, dass Sie dies den Entscheidungsträgern erwähnen.

4
Nkosi

Zunächst einmal sollten Sie - Erfassung dieses Objekts über eine Schnittstelle vornehmen, wenn der Code dort gerade aus dem DB abruft. Dann können Sie diese Schnittstelle simulieren, um zurückzugeben, was Sie in Ihren Gerätetests möchten.

Wenn ich in Ihren Schuhen wäre, würde ich die tatsächliche berechnung - Logik extrahieren und Tests für diese neue "Rechner" -Klasse (n) schreiben. Zerstöre alles so viel du kannst. Wenn die Eingabe 100 Eigenschaften hat, aber nicht alle davon für jede Berechnung relevant sind, verwenden Sie Interfaces , um sie aufzuteilen. Dadurch wird die erwartete Eingabe sichtbar und der Code wird ebenfalls verbessert.

Wenn Ihre Klasse beispielsweise BigClass heißt, können Sie eine Schnittstelle erstellen, die in einer bestimmten Berechnung verwendet wird. Auf diese Weise ändern Sie nicht die vorhandene Klasse oder die Art und Weise, wie der andere Code damit arbeitet. Die extrahierte Rechnerlogik wäre unabhängig, überprüfbar und der Code - viel einfacher.

    public class BigClass : ISet1
    {
        public string Prop1 { get; set; }
        public string Prop2 { get; set; }
        public string Prop3 { get; set; }
    }

    public interface ISet1
    {
        string Prop1 { get; set; }

        string Prop2 { get; set; }
    }

    public interface ICalculator
    {
        CalculationResult Calculate(ISet1 input)
    }
1
Stefan Georgiev

Ich würde diesen Ansatz wählen:

1 - Schreiben Sie Komponententests für jede Kombination des 100 Eigenschafteneingabeparameterobjekts. Nutzen Sie dazu ein Werkzeug, um dies für Sie zu tun (z. B. pex, intellitest) und stellen Sie sicher, dass alle grün laufen. An dieser Stelle beziehen sich die Komponententests eher auf Integrationstests als auf Komponententests, aus Gründen, die später offensichtlich werden.

2 - Refactor der Tests in SOLID Codeabschnitte - Methoden, die keine anderen Methoden aufrufen, können als echtes Unit-Testing betrachtet werden, da sie keine Abhängigkeiten von anderem Code haben. Die übrigen Methoden sind nur noch auf Integration prüfbar.

3 - Stellen Sie sicher, dass ALLE Integrationstests noch grün ausgeführt werden.

4 - Erstellen Sie neue Komponententests für den neuen Unit-Testable-Code.

5 - Wenn alles grün läuft, können Sie alle/einige der überflüssigen ursprünglichen Integrationstests löschen - nur Sie, wenn Sie sich dazu wohl fühlen.

6 - Wenn alles grün läuft, können Sie anfangen, die 100 für die Unit-Tests erforderlichen Eigenschaften auf nur die für die einzelnen Methoden unbedingt erforderlichen Eigenschaften zu reduzieren. Dies wird wahrscheinlich Bereiche für zusätzliches Refactoring hervorheben, aber das Parameterobjekt wird auf jeden Fall vereinfacht. Dies wiederum wird die Bemühungen künftiger Programmierer von Codecs weniger fehleranfällig machen, und ich würde wetten, dass historische Ausfälle, die Größe des Parameterobjekts anzusprechen, wenn es 50 Eigenschaften hatte, der Grund dafür sind, dass es jetzt 100 sind. Ich werde schließlich auf 150 Parameter anwachsen, die es sich zeigen lassen, will niemand.

1
Greg Trevellick

Verwenden Sie eine In-Memory-Datenbank für Ihre Gerätetests

Also ... das ist technisch keine Antwort, wie Sie gesagt haben, Unit-Tests, und durch die Verwendung einer In-Memory-Datenbank werden Integrationstests durchgeführt, nicht Unit-Tests. Ich finde jedoch, dass manchmal, wenn Sie mit unmöglichen Einschränkungen konfrontiert sind, Sie irgendwo hin geben müssen, und dies kann eine dieser Zeiten sein.

Mein Vorschlag ist die Verwendung von SQLite (oder ähnlichem) bei den Komponententests. Es gibt Tools zum Extrahieren und Duplizieren Ihrer aktuellen Datenbank in eine SQLite-Datenbank. Anschließend können Sie die Skripts generieren und in eine In-Memory-Version der Datenbank laden. Sie können die Abhängigkeitsinjektion und das Repository-Muster verwenden, um den Datenbankanbieter in Ihren "Unit" -Tests anders einzustellen als im realen Code. 

Auf diese Weise können Sie Ihre vorhandenen Daten verwenden und bei Bedarf als Voraussetzungen für Ihre Tests modifizieren. Sie müssen anerkennen, dass dies kein wahrer Komponententest ist ... was bedeutet, dass Sie auf das beschränkt sind, was die Datenbank wirklich generieren kann (d. H. Tabelleneinschränkungen verhindern das Testen bestimmter Szenarien). Sie können also keinen vollständigen Komponententest in diesem Sinne durchführen. Diese Tests laufen auch langsamer, da sie wirklich Datenbankarbeit leisten. Sie müssen also die zusätzliche Zeit einplanen, die für die Ausführung dieser Tests erforderlich ist. (Obwohl sie normalerweise immer noch ziemlich schnell sind.) Beachten Sie, dass Sie auch andere Entitäten simulieren können (z. B. wenn zusätzlich zur Datenbank ein Serviceabruf vorliegt, ist dies immer noch ein Scheinpotenzial).

Wenn Ihnen dieser Ansatz nützlich erscheint, finden Sie hier einige Links, die Sie zum Laufen bringen.

SQL Server in SQLite-Konverter:

https://www.codeproject.com/Articles/26932/Convert-SQL-Server-DB-nach-SQLite-DB

SQLite Studio: https://sqlitestudio.pl/index.rvt

(Verwenden Sie das, um Ihre Skripte für die Verwendung im Arbeitsspeicher zu generieren.)

So verwenden Sie den Speicher:

TestConnection = neue SQLiteConnection ("FullUri = Datei :: Speicher:? Cache = gemeinsam genutzt");

Ich habe ein separates Skript für die Datenbankstruktur vom Datenladen, aber das ist eine persönliche Präferenz.

Hoffe das hilft und viel Glück.

0
Reginald Blue