Sollten Sie Ihre Daten über alle Komponententests hinweg fest codieren?

33

Bei den meisten Unit-Testing-Tutorials / Beispielen werden in der Regel die zu testenden Daten für jeden einzelnen Test definiert. Ich denke, dies ist Teil der Theorie "Alles sollte isoliert getestet werden".

Ich habe jedoch festgestellt, dass bei mehrschichtigen Anwendungen mit viel DI der Code, der zum Einrichten der einzelnen Tests erforderlich ist, sehr langwierig ist. Stattdessen habe ich eine Reihe von Testbase-Klassen erstellt, die ich jetzt erben kann und die viele vorgefertigte Testgerüste haben.

Als Teil davon erstelle ich auch gefälschte Datasets, die die Datenbank einer laufenden Anwendung darstellen, wenngleich normalerweise nur eine oder zwei Zeilen in jeder "Tabelle" enthalten sind.

Ist es eine akzeptierte Praxis, die Mehrzahl der Testdaten über alle Komponententests hinweg vorab zu definieren, wenn nicht sogar alle?

Aktualisieren

Aus den Kommentaren unten geht hervor, dass ich mehr Integration als Unit-Tests mache.

Mein aktuelles Projekt ist ASP.NET MVC mit Unit of Work über Entity Framework Code First und Moq zum Testen. Ich habe die UOW und die Repositorys verspottet, aber ich verwende die echten Geschäftslogikklassen und teste die Controlleraktionen. Bei den Tests wird häufig überprüft, ob die UoW festgeschrieben wurde, z.

[TestClass]
public class SetupControllerTests : SetupControllerTestBase {
  [TestMethod]
  public void UserInvite_ExistingUser_DoesntInsertNewUser() {
    // Arrange
    var model = new Mandy.App.Models.Setup.UserInvite() {
      Email = userData.First().Email
    };

    // Act
    setupController.UserInvite(model);

    // Assert
    mockUserSet.Verify(m => m.Add(It.IsAny<UserProfile>()), Times.Never);
    mockUnitOfWork.Verify(m => m.Commit(), Times.Once);
  }
}

SetupControllerTestBasebaut die Schein-UOW und instanziiert die userLogic.

Viele der Tests setzen voraus, dass ein Benutzer oder ein Produkt in der Datenbank vorhanden ist. In diesem Beispiel habe ich bereits angegeben, was die nachgebildete Benutzeroberfläche zurückgibt. Dies userDataist lediglich ein IList<User>Datensatz für einen einzelnen Benutzer.

Mattdwen
quelle
4
Das Problem bei Tutorials / Beispielen ist, dass sie einfach sein müssen, aber Sie können keine Lösung für ein komplexes Problem an einem einfachen Beispiel zeigen. Sie sollten von "Fallstudien" begleitet werden, in denen beschrieben wird, wie das Tool in realen Projekten mit angemessener Größe eingesetzt wird, was jedoch selten der Fall ist.
Jan Hudec
Vielleicht könnten Sie ein paar kleine Beispiele für Code hinzufügen, über die Sie nicht ganz glücklich sind.
Luc Franken
Wenn Sie viel Setup-Code benötigen , um einen Test auszuführen, laufen Sie Gefahr, einen Funktionstest auszuführen. Wenn der Test fehlschlägt, wenn Sie den Code ändern, der Code jedoch nicht fehlerhaft ist. Es ist definitiv ein Funktionstest.
Reactgular
Das Buch "xUnit Test Patterns" ist ein starkes Argument für wiederverwendbare Geräte und Helfer. Testcode sollte genauso wartbar sein wie jeder andere Code.
Chuck Krutsinger
Dieser Artikel kann hilfreich sein: yegor256.com/2015/05/25/unit-test-scaffolding.html
yegor256

Antworten:

25

Letztendlich möchten Sie so wenig Code wie möglich schreiben, um möglichst viele Ergebnisse zu erzielen. Wenn Sie in mehreren Tests viel denselben Code haben, führt a) in der Regel zu einer Codierung durch Kopieren und Einfügen, und b) wenn sich eine Methodensignatur ändert, müssen Sie möglicherweise viele fehlerhafte Tests reparieren.

Ich verwende den Ansatz, Standard-TestHelper-Klassen zu haben, die mir viele der Datentypen liefern, die ich routinemäßig verwende, damit ich Sätze von Standardentitäts- oder DTO- Klassen für meine Tests erstellen kann, um abzufragen und genau zu wissen, was ich jedes Mal bekomme. Ich kann also anrufen TestHelper.GetFooRange( 0, 100 ), um eine Auswahl von 100 Foo-Objekten mit all ihren abhängigen Klassen / Feldern zu erhalten.

Insbesondere, wenn in einem ORM-Typsystem komplexe Beziehungen konfiguriert sind, die vorhanden sein müssen, damit die Dinge ordnungsgemäß ausgeführt werden, für diesen Test jedoch nicht unbedingt von Bedeutung sind, wodurch viel Zeit gespart werden kann.

In Situationen, in denen ich in der Nähe der Datenebene teste, erstelle ich manchmal eine Testversion meiner Repository-Klasse, die auf ähnliche Weise abgefragt werden kann (auch dies ist in einer ORM-Umgebung der Fall und wäre für a nicht relevant) reale Datenbank), da das Verspotten der genauen Antworten auf Abfragen viel Arbeit kostet und oft nur geringe Vorteile bietet.

Bei Unit-Tests gilt es jedoch einige Dinge zu beachten:

  • Stellen Sie sicher , dass Ihre Mocks sind spottet . Die Klassen, die Operationen für die zu testende Klasse ausführen, müssen Scheinobjekte sein, wenn Sie Unit-Tests durchführen. Ihre DTO- / Entitätstypklassen können die Realität sein, aber wenn Klassen Vorgänge ausführen, müssen Sie sie verspotten. Andernfalls müssen Sie viel länger suchen, um herauszufinden, welche Änderungen fehlschlagen, wenn sich der unterstützende Code ändert und Ihre Tests fehlschlagen hat das Problem tatsächlich verursacht.
  • Stellen Sie sicher, dass Sie Ihre Klassen testen . Wenn man sich eine Reihe von Unit-Tests ansieht, stellt sich manchmal heraus, dass die Hälfte der Tests das Mocking-Framework mehr testet als den eigentlichen Code, den sie testen sollen.
  • Nicht wiederverwenden von Schein- / Unterstützungsobjekten Dies ist ein großes Problem, wenn man versucht, mit Code-unterstützenden Unit-Tests klug zu werden, ist es wirklich einfach, versehentlich Objekte zu erstellen, die zwischen Tests bestehen bleiben und unvorhersehbare Auswirkungen haben können. Zum Beispiel hatte ich gestern einen Test, der bestanden wurde, als er alleine ausgeführt wurde, bestanden wurde, als alle Tests in der Klasse ausgeführt wurden, aber fehlgeschlagen war, als die gesamte Testsuite ausgeführt wurde. Es stellte sich heraus, dass ein Testhelfer ein hinterhältiges statisches Objekt enthielt, das bei seiner Erstellung definitiv nie ein Problem verursacht hätte. Denken Sie daran: Zu Beginn des Tests wird alles erstellt, am Ende des Tests wird alles zerstört.
Glenatron
quelle
10

Was auch immer die Absicht Ihres Tests lesbarer macht.

Als allgemeine Faustregel gilt:

Wenn die Daten Teil des Tests sind (z. B. sollten keine Zeilen mit dem Status 7 gedruckt werden), codieren Sie sie im Test, damit klar ist, was der Autor beabsichtigt hat.

Wenn es sich bei den Daten nur um Fülldaten handelt, um sicherzustellen, dass sie zu bearbeiten sind (z. B. sollte der Datensatz nicht als vollständig markiert werden, wenn der Verarbeitungsdienst eine Ausnahme auslöst), sollten Sie auf jeden Fall eine BuildDummyData-Methode oder eine Testklasse verwenden, die irrelevante Daten aus dem Test heraushält .

Aber beachte, dass ich Mühe habe, mir ein gutes Beispiel für Letzteres auszudenken. Wenn Sie viele davon in einem Unit-Test-Gerät haben, haben Sie wahrscheinlich ein anderes Problem zu lösen ... Vielleicht ist die zu testende Methode zu komplex.

pdr
quelle
+1 Ich stimme zu. Das riecht nach dem, was er testet, ist für Unit-Tests zu eng gekoppelt.
Reactgular
5

Verschiedene Testmethoden

Definieren Sie zunächst, was Sie tun: Unit-Test oder Integrationstest . Die Anzahl der Schichten ist für Komponententests irrelevant, da Sie höchstwahrscheinlich nur eine Klasse testen. Den Rest verspotten Sie. Für Integrationstests ist es unvermeidlich, dass Sie mehrere Ebenen testen. Wenn Sie über gute Komponententests verfügen, besteht der Trick darin, die Integrationstests nicht zu komplex zu gestalten.

Wenn Ihre Komponententests gut sind, müssen Sie beim Integrationstest nicht alle Details wiederholen.

Begriffe, die wir verwenden, sind ein bisschen plattformabhängig, aber Sie können sie in fast allen Test- / Entwicklungsplattformen finden:

Beispielanwendung

Je nach verwendeter Technologie können die Namen unterschiedlich sein, ich werde dies jedoch als Beispiel verwenden:

Wenn Sie eine einfache CRUD-Anwendung mit Modell Product, ProductsController und einer Indexansicht haben, die eine HTML-Tabelle mit Produkten generiert:

Das Endergebnis der Anwendung ist eine HTML-Tabelle mit einer Liste aller aktiven Produkte.

Unit-Test

Modell

Das Modell kannst du ganz einfach testen. Es gibt verschiedene Methoden dafür; Wir verwenden Vorrichtungen. Ich denke, das nennt man "gefälschte Datensätze". Also erstellen wir vor jedem Test die Tabelle und geben die Originaldaten ein. Die meisten Plattformen haben Methoden dafür. Zum Beispiel in Ihrer Testklasse eine Methode setUp (), die vor jedem Test ausgeführt wird.

Dann führen wir unseren Test durch, zum Beispiel: testGetAllActive- Produkte.

Also testen wir direkt in eine Testdatenbank. Wir verspotten die Datenquelle nicht. Wir machen es immer gleich. Dies ermöglicht es uns beispielsweise, mit einer neuen Version der Datenbank zu testen, und es treten Fragen auf.

In der realen Welt kann man nicht immer einer 100% igen Einzelverantwortung folgen . Wenn Sie dies noch besser machen möchten, könnten Sie eine Datenquelle verwenden, die Sie verspotten. Für uns (wir verwenden ein ORM), das das Gefühl hat, bereits vorhandene Technologie zu testen. Außerdem werden die Tests viel komplexer und testen die Abfragen nicht wirklich. Also halten wir es so.

Die fest codierten Daten werden separat in den Fixtures gespeichert. Das Fixture ist also wie eine SQL-Datei mit einer create table-Anweisung und Einfügungen für die von uns verwendeten Datensätze. Wir halten sie klein, es sei denn, es besteht ein tatsächlicher Testbedarf mit vielen Aufzeichnungen.

class ProductModel {
  public function getAllActive() {
    return $this->find('all', array('conditions' => array('active' => 1)));
  }
}

Regler

Der Controller benötigt mehr Arbeit, da wir das Modell damit nicht testen wollen. Also verspotten wir das Modell. Das heißt: Wir testen die Methode: index (), die eine Liste von Datensätzen zurückgeben soll.

Also verspotten wir die Modellmethode getAllActive () und fügen feste Daten hinzu (zum Beispiel zwei Datensätze). Jetzt testen wir die Daten, die der Controller an die Ansicht sendet, und vergleichen, ob wir diese beiden Datensätze wirklich zurückbekommen.

function testProductIndexLoggedIn() {
  $this->setLoggedIn();
  $this->ProductsController->mock('ProductModel', 'index', function(return array(your records) ));
  $result=$this->ProductsController->index();
  $this->assertEquals(2, count($result['products']));
}

Das ist genug. Wir versuchen, dem Controller so wenig Funktionalität wie möglich hinzuzufügen, da dies das Testen schwierig macht. Aber natürlich ist immer etwas Code drin. Zum Beispiel testen wir Anforderungen wie: Zeigen Sie diese beiden Datensätze nur an, wenn Sie angemeldet sind.

Der Controller benötigt also normalerweise einen Schein und ein kleines Stück fest codierter Daten. Für ein Login-System vielleicht ein anderes. In unserem Test haben wir eine Hilfsmethode dafür: setLoggedIn (). Das macht es einfach, mit oder ohne Login zu testen.

class ProductsController {
  public function index() {
    if($this->loggedIn()) {
      $this->set('products', $this->ProductModel->getAllActive());
    }
  }
}

Ansichten

Views testen ist schwer. Zuerst trennen wir die sich wiederholende Logik. Wir setzen es in Helfer und testen diese Klassen streng. Wir erwarten immer die gleiche Leistung. Beispiel: generateHtmlTableFromArray ().

Dann haben wir einige projektspezifische Ansichten. Diese testen wir nicht. Es ist nicht wirklich erwünscht, solche Einheiten zu testen. Wir bewahren sie für Integrationstests auf. Da wir einen Großteil des Codes in Views herausgenommen haben, haben wir hier ein geringeres Risiko.

Wenn Sie anfangen, diese zu testen, müssen Sie Ihre Tests wahrscheinlich jedes Mal ändern, wenn Sie ein Stück HTML ändern, was für die meisten Projekte nicht nützlich ist.

echo $this->tableHelper->generateHtmlTableFromArray($products);

Integrationstests

Abhängig von Ihrer Plattform können Sie hier mit User Stories usw. arbeiten. Es kann webbasiert sein wie Selenium oder andere vergleichbare Lösungen.

Im Allgemeinen laden wir einfach die Datenbank mit den Fixtures und geben an, welche Daten verfügbar sein sollen. Für vollständige Integrationstests verwenden wir im Allgemeinen sehr globale Anforderungen. Also: Produkt auf aktiv setzen und dann prüfen, ob das Produkt verfügbar ist.

Wir testen nicht alles noch einmal, zum Beispiel, ob die richtigen Felder verfügbar sind. Hier testen wir die größeren Anforderungen. Da wir unsere Tests nicht vom Controller oder aus der Sicht duplizieren wollen. Wenn etwas wirklich der Schlüssel / Kernbestandteil Ihrer Anwendung ist oder aus Sicherheitsgründen (das Kennwort ist NICHT verfügbar), fügen wir es hinzu, um sicherzustellen, dass es richtig ist.

Die fest codierten Daten werden in den Fixtures gespeichert.

function testIntegrationProductIndexLoggedIn() {
  $this->setLoggedIn();
  $result=$this->request('products/index');

  $expected='<table';
  $this->assertContains($expected, $result);

  // Some content from the fixture record
  $expected='<td>Product 1 name</td>';
  $this->assertContains($expected, $result);
}
Luc Franken
quelle
Dies ist eine großartige Antwort auf eine ganz andere Frage.
pdr
Danke für die Rückmeldung. Sie könnten Recht haben, dass ich es nicht zu spezifisch erwähnt habe. Der Grund für die ausführliche Antwort ist, dass ich beim Testen der gestellten Frage eines der schwierigsten Dinge sehe. Die Übersicht darüber, wie das Testen für sich genommen zu den verschiedenen Arten von Tests passt. Deshalb habe ich in jedem Teil hinzugefügt, wie die Daten behandelt (oder herausgetrennt) werden. Werde mal schauen, ob ich es klarer kriegen kann.
Luc Franken
Die Antwort wurde mit einigen Codebeispielen aktualisiert, um zu erläutern, wie Sie testen können, ohne alle möglichen anderen Klassen aufzurufen.
Luc Franken
4

Wenn Sie Tests schreiben, die viel DI und Verkabelung erfordern, bis hin zur Verwendung "echter" Datenquellen, haben Sie wahrscheinlich den Bereich der einfachen Einheitentests verlassen und sind in den Bereich der Integrationstests eingetreten.

Für Integrationstests ist es meiner Meinung nach keine schlechte Idee, eine gemeinsame Dateneinrichtungslogik zu haben. Das Hauptziel solcher Tests ist der Nachweis, dass alles richtig konfiguriert ist. Dies ist ziemlich unabhängig von den konkreten Daten, die über Ihr System gesendet werden.

Für Unit-Tests hingegen würde ich empfehlen, das Ziel einer Testklasse als eine einzige "echte" Klasse zu halten und alles andere zu verspotten. Dann sollten Sie die Testdaten wirklich hart codieren, um sicherzustellen, dass Sie so viele spezielle / vorherige Fehlerpfade wie möglich zurückgelegt haben.

Um Tests ein semi-hartcodiertes / zufälliges Element hinzuzufügen, möchte ich zufällige Modellfabriken vorstellen. In einem Test mit einer Instanz meines Modells benutze ich diese Fabriken, um ein gültiges, aber völlig zufälliges Modellobjekt zu erstellen und dann nur die Eigenschaften hart zu codieren, die für den vorliegenden Test von Interesse sind. Auf diese Weise geben Sie alle relevanten Daten direkt in Ihrem Test an. Gleichzeitig müssen Sie nicht alle irrelevanten Daten angeben und (bis zu einem gewissen Grad) überprüfen, ob keine unbeabsichtigten Abhängigkeiten von anderen Modellfeldern bestehen.

Sven Amann
quelle
-1

Ich denke, es ist ziemlich üblich, die meisten Daten für Ihre Tests hart zu codieren.

Stellen Sie sich eine einfache Situation vor, in der ein bestimmter Datensatz einen Fehler verursacht. Sie können speziell einen Komponententest für diese Daten erstellen, um die Korrektur durchzuführen und sicherzustellen, dass der Fehler nicht wieder auftritt. Im Laufe der Zeit werden Ihre Tests eine Reihe von Daten enthalten, die eine Reihe von Testfällen abdecken.

Mit vordefinierten Testdaten können Sie auch einen Datensatz erstellen, der eine Vielzahl bekannter Situationen abdeckt.

Trotzdem denke ich, dass es auch sinnvoll ist, zufällige Daten in Ihren Tests zu haben.

Sasbury
quelle
Hast du die Frage wirklich gelesen und nicht nur den Titel?
Jakob
Es ist wichtig, zufällige Daten in Ihren Tests zu haben - Ja, denn es gibt nichts Schöneres, als herauszufinden, was in einem Test passiert ist, wenn dieser einmal pro Woche fehlschlägt.
pdr
Es ist sinnvoll, zufällige Daten in Ihren Tests für Trübungs- / Fuzzing- / Eingangstests zu haben. Aber nicht in Ihren Unit-Tests, das wäre ein Albtraum.
Glenatron