Modultests

Einzeltests

Die Entwicklung eines Projekts mit Modultests flankieren

Entdecken Sie in diesem Artikel den Nutzen einer der unbekanntesten Facetten der Entwicklung, die jedoch für einige Projekte die Rettung sein könnte: Tests.

Was ist ein Test?

Ein Test ist eine besondere Funktion, die in einer speziellen Datei und in einem Ordner speziell für Tests (im allgemeinen „“Tests““ genannt und an der Basis des in der Entwicklung befindlichen Projekts platziert) geschrieben wird. Es geht darum, eine Abfolge von Regeln festzulegen, die auf eine im Projekt vorhandene Funktion unserer Wahl anzuwenden sind und sich zu vergewissern, dass sie heute oder auch in zwei Jahren noch korrekt funktioniert. Das Prinzip besteht darin, die erwarteten Ergebnisse mit den tatsächlich erhaltenen Ergebnissen zu vergleichen. Das mag sich in dieser Erklärung sehr unspektakulär anhören, jedoch können die Tests wahre Wunder vollbringen, wie zum Beispiel verhindern, dass Projekte abdriften und untergehen. Sie werden gleich sehen, warum.

Es gibt mehrere Arten von Tests:

  • Die Modultests
  • Die Funktionstests

Die Modultests ermöglichen es, eine besondere Funktion zu testen und sicherzustellen, dass sie korrekt abläuft. Das Prinzip besteht darin, eine kritische Funktion auszuwählen, sie unter allen Gesichtspunkten zu testen und die Ergebnisse mit den erwarteten Ergebnissen zu vergleichen. Ziel ist es hier, die Nachhaltigkeit des Projekts dadurch zu gewährleisten, dass unabhängig von den eingebrachten Änderungen bei zukünftigen Weiterentwicklungen die zugeordnete Methode weiterhin normal und ohne Regressionen arbeitet, solange dieser Test auf Grün steht. Beispiel: Wenn ich eine Funktion entwickelt habe, die als Eingabe eine ID benötigt und mir als Ausgabe einen Benutzer angibt, wird mein Modultest testen, dass ich auch wirklich einen Nutzer erhalte, wenn ich die ID 1 eingebe, dass ich NULL erhalte, wenn ich die ID NULL eingebe (und es somit nicht zu einem Systemabsturz kommt) etc…

Mit den Funktionstests als Gegenstück zu den Modultests vergewissert man sich über den korrekten Ablauf eines Prozesses. Hier geht es darum, eine wichtige Abfolge von Schritten zu kontrollieren und sich zu vergewissern, dass kein Fehler auftritt. Beispiel: Wenn ich eine Seite entwickelt habe, die es ermöglicht, ein Nutzerprofil in Abhängigkeit von der in der URL angegebenen ID anzuzeigen, werde ich diese URL mit dem ID 1 testen und mich vergewissern, dass es sich wirklich um das Profil des Benutzers 1 handelt, das auf dem Display angezeigt wird. Auf dieselbe Art werde ich diese URL auch mit einer leeren oder einer nicht existierenden ID testen und mich vergewissern, dass ich auch wirklich den Fehler 404 erhalte.

Pro und Contra

Wenn man eine Website oder eine Anwendung entwickelt, ist es sehr zu empfehlen, Tests zu schreiben, um das einwandfreie Funktionieren einer Methode oder einer Aktion sicherzustellen.
Leider gibt es nur allzu viele Entwickler, die diese Best Practice ignorieren. Die Ausreden sind zahlreich und häufig zutreffend:

  • Es dauert (sehr) lange
  • Es ist aufwändig
  • Man muss die Tests neu schreiben, wenn man das Verhalten einer Funktion ändert
  • Keine Lust (die bisher am häufigsten gehörte Entschuldigung)

Aber trotzdem können sich Modultests auch als so nützlich erweisen, dass sie in der Lage sind, Projekte zu retten und das auf mehrfache Art und Weise:

  • Die Entwicklung eines Projekts flankieren
  • Vor später auftretenden Regressionen schützen
  • Die Nutzungsmöglichkeiten des Projekts ausdrücklich festlegen
  • Unzulässigen Nutzungsfällen vorbeugen (diese nicht vorgesehenen Nutzungen bringen die in Produktion befindliche Website zum Absturz)

Wann ist der beste Zeitpunkt?

Die Good Practices empfehlen, für jede entwickelte Funktion immer auch einen Test zu entwickeln und den Test immer vor der Funktion zu schreiben (der Test wird also fehlschlagen, bis die Funktion beendet ist). Auch ich muss jedoch gestehen, dass es bei mir oft vorkommt, dass ich die Idee während der Umsetzung komplett ändere, was natürlich nicht besonders praktisch wäre, wenn ich die Tests schon vorab geschrieben hätte. Daher gilt, dass es zwar an sich eine gute Sache ist, die Tests vorab abzufassen, ich ziehe es aber vor, sie erst hinterher zu schreiben, wenn das System perfekt arbeitet.

Es wird im Allgemeinen empfohlen, mit dem Schreiben der Tests gleich zu Beginn des Projekts anzufangen. Für jede neue Funktion fügt man einen neuen Test hinzu. Das Schreiben von Tests hat natürlich seinen Preis, und das nicht zu knapp. Man muss im Durchschnitt damit rechnen, dass zwischen 10% und 20% der Zeit, die für die Erstellung einer Aufgabe eingeräumt war, für das Abfassen der zugehörigen Tests benötigt wird, wodurch sich die Dauer der Projektentwicklung verlängert.

„Ein Beispiel aus der Praxis
Stellen wir uns einmal vor, dass wir eine E-Commerce-Website entwickeln. Es wäre angebracht, die einzelnen Funktionen zu testen, die mit dem Hinzufügen eines Produkts zum Einkaufswagen verbunden sind. Wir hätten somit folgende Modultests:
testGetProduct
testGetCart
testAddProductToCart
Für alle folgenden Beispiele werde ich die im Symfony-Framework zur Verfügung gestellten Funktionen verwenden. Es gibt viele Lösungen, die Tools zur Durchführung von Modultests anbieten und fast jede dieser Lösungen hat ähnliche Funktionen und eine ähnliche Syntax. Für diesen Artikel fiel meine Wahl auf Symfony und seine Implementierung von PHPUnit (Test-Bibliothek in PHP).
Für die Testfunktion testGetProduct, die testen soll, wie man ein Produkt erhält, hier ein Beispiel für einen verwendbaren Code:

public function testGetProduct() {
    $this->assertInstanceOf(
        'Product',
        $this->catalogService->getProduct(1)
    );
    $this->assertNull(
        $this->catalogService->getProduct(null)
    );
    $this->assertNull(
        $this->catalogService->getProduct(9999)
    );
    $this->assertNull(
        $this->catalogService->getProduct('banana')
    );
 }

In diesem Beispiel testen wir alle vorstellbaren Anwendungsfälle unserer Funktion getProduct():

  • Zunächst die gültige Art, das heißt mit einer digitalen und existierenden ID, die ein Ergebnisobjekt des Typs „“Produkt““ zurückschicken MUSS und nicht eine Tabelle oder etwas anderes in dieser Art
  • Dann der Fall, in dem es keine mitgelieferte ID gibt, was mit großer Wahrscheinlichkeit eintreten kann, wenn die ID in der URL fehlt und es keinen Vorab-Test gibt (was nicht gut ist), man testet aber trotzdem, nur um das Verhalten zu gewährleisten und schwarz auf weiß zu schreiben, dass „“keine ID““ gleichbedeutend ist mit „“kein Ergebnis““ (und nicht ein leeres Objekt oder schlimmer, ein Absturz)
  • Danach kommt der Fall einer nicht existierenden ID, wie zum Beispiel, wenn ein kleiner Schlaumeier sich einen Spaß daraus macht, die ID in der URL zu ändern
  • Man weiß ja nie, vielleicht ist unser Benutzer ein kleines bisschen hungrig und möchte einen Text anstelle der ID setzen – wir vergewissern uns nur, dass dies nicht die ganze Website zum Absturz bringt…

Die Funktion testGetCart stellt sicher, dass bei der Funktion getCart() auch wirklich ein Einkaufswagen zurückkommt. Das scheint offensichtlich und ein Test somit nicht nötig zu sein, aber wenn in 3 Monaten jemand beschließt, das Objekt „“Einkaufswagen““ durch eine Tabelle zu ersetzen, wird sich dies auf alle Funktionen der Website auswirken, die direkt oder indirekt mit dem Einkaufswagen in Verbindung stehen. Es ist also besser, schwarz auf weiß zu schreiben, dass die komplette Website darauf ausgerichtet ist, dass getCart() ein Objekt des Typs „“Einkaufswagen““ zurücksendet und wenn jemand auf die Idee kommt, die Art der Rücksendung zu ändern, dann geschieht das auf dessen eigene Verantwortung.

public function testGetCart() {
    $this->assertInstanceOf(
        'Cart',
        $this->catalogService->getCart()
    );
}

In diesem Beispiel führen wir ganz einfach einen einzigen Test an unserer Funktion getCart() durch, nämlich:

  • Wir bestätigen, dass es sich wirklich um ein Objekt des Typs „Einkaufswagen“ handelt.

Und schließlich testet die Funktion testAddProductToCart das Hinzufügen eines Produkts zum Einkaufswagen und prüft das korrekte Systemverhalten sowohl mit gültigen als auch mit ungültigen Daten.

public function testAddProductToCart() {
    $cart = $this->catalogService->getCart();
    $product = $this->catalogService->getProduct(1);

    $this->assertTrue(
        $cart->addProduct($product)
    );
    $this->assertFalse(
        $cart->addProduct(null)
    );
    $this->assertFalse(
        $cart->addProduct(1)
    );
    $this->assertFalse(
        $cart->addProduct('banana')
    );
}

In diesem Beispiel kontrollieren wir alle vorstellbaren Nutzungsfälle für das Hinzufügen eines Produkts zum Einkaufswagen:

  • Zuerst vergewissert man sich, dass die Funktion entsprechend ihrer vorgesehenen späteren Nutzung richtig arbeitet, das heißt, man gibt ihr ein Produkt und hofft, dass sie TRUE zurück liefert.
  • Danach prüft man, ob die Funktion auch wirklich FALSE zurück liefert, wenn man ihr kein Produkt gibt
  • Weiterhin erwartet man auch ein FALSE, wenn man die ID des Produkts statt des Produkts selbst gibt
  • Und schließlich, man weiß zwar nicht so richtig, wie es dazu kommen sollte, aber es ist auf jeden Fall besser zu prüfen, ob die Funktion addProduct sich richtig verhält mit einer Kette von Buchstaben statt des Produkts

Nachdem wir nun die Methoden getestet haben, muss der Vorgang des Hinzufügens eines Produkts zu einem Einkaufswagen mithilfe eines Funktionstests getestet werden.

Diese Art Test ist in seiner Durchführung vager, denn er hängt sowohl von der verwendeten Programmiersprache, von dem für die Tests eingesetzten Tool und von der Art der Ausführung des Prozesses ab. Hier werden wir wieder das Testsystem Symfony mit seinem Crawler verwenden, der verschiedene URLs aufrufen wird, um den Pfad für das Hinzufügen eines Produkts in den Einkaufswagen zu simulieren.

public function testAddProductToCart() {
    $client = static::createClient();
    $client->followRedirects( true );

    $productPage = $client->request( 'GET', '/products/1' );
    $addToCartLink = $productPage
                         ->filter( 'a#add-to-cart' )
                         ->link();

    $cartPage = $client->click( $addToCartLink );

    $banana = $cartPage->filter( 'span:contains("Banana")' );

    $this->assertEquals( 1, $banana );
}
  • Dieser Test ist ein wenig komplexer als die vorangehenden, aber wir werden ihn gemeinsam analysieren:
  • Zunächst legen wir einen „“Kunden““ an, der die Aufgabe hat, durch die URLs der Website zu navigieren und der den HTML-Code dieser Seiten zurückliefert
  • Nebenbei nutzen wir auch die Gelegenheit, um ihm zu sagen, dass er allen „“Weiterleitungen““ folgen soll, sofern vorhanden
  • Dann gehen wir zu den ernsteren Dingen über, indem wir dem Kunden angeben, dass er die Seite eines Produkts besuchen soll
  • Anschließend sucht man im HTML den Link, der es ermöglicht, das Produkt in den Einkaufswagen zu legen
  • Wir klicken auf den Link (mit dem das Produkt in den Einkaufswagen gelegt wird, bevor wir wieder auf die Liste der Produkte des Einkaufswagens zurückgeführt werden)
  • Wir lesen alle Tags aus, die den Text „“Banana““ enthalten
  • Und schließlich vergewissern wir uns, dass dieser auf der gesamten Seite einmal vorkommt, und zwar nur ein einziges Mal

Und zum Schluss, wenn aus irgendeinem Grund eine der beim Vorgang des Hinzufügens eines Produkts zum Einkaufswagen verwendeten Funktionen sich plötzlich ändern sollte (Fehler oder absichtliche Änderung), ob das nun in einer Woche oder in 2 Jahren der Fall sein wird, dann erhalten die Entwickler eine Warnung, dass dieser Teil der Seite kaputt ist und sie können die erforderlichen Korrekturen an der Seite durchführen, bevor diese wieder in Betrieb geht.

Fixtures

In allen meinen vorausgehenden Erklärungen habe ich immer so getestet, dass das Produkt mit der ID 1 ein „“Banana““-Produkt war. Aber wäre dies nicht gefährlich mit Daten aus der Datenbank, die sich mit der Zeit ändern sollen?
Antwort: Ja, und sogar sehr gefährlich. Aus diesem Grund darf man niemals einen Test mit Prod-Daten machen.

Aber wo sind dann die Daten? An dieser Stelle kommen jetzt die Fixtures ins Spiel.

Fixtures sind vorab definierte und automatisch vor jeder Testausführung installierbare Basisdaten. Ziel ist es, eine saubere Datenbank für Tests zu erhalten, die nur feste, bekannte und vorab definierte Daten enthält. Die einzelnen Technologien schlagen fast alle ähnliche Lösungen unter unterschiedlichen Namen vor, das Prinzip bleibt jedoch gleich: Basisdaten in die Datenbank einfügen.

Mocks

Man bräuchte einen ganzen Artikel zu diesem Thema, um Mocks zu beschreiben, aber ich werde das Prinzip in einigen Worten zusammenfassen.

Es kommt gelegentlich vor, dass manche Funktionen aufgrund bestimmter Methoden oder der Unmöglichkeit, den Test kontrolliert auszuführen, nicht testbar sind. In diesen Fällen ist die Verwendung von Mocks für die Simulation dieser Objekte wichtig.

Stellen wir uns einmal vor, dass zu einem bestimmten Augenblick in der normalen Ausführung der Anwendung über eine cURL-Anfrage eine Umleitung auf eine andere Seite erfolgt und dass wir dieses Ergebnis in der weiteren Folge des Prozesses verwenden.
Es wäre nicht hinnehmbar, solche variablen Daten im Rahmen eines Tests zu verwenden, der eigentlich unter kontrollierten Bedingungen durchgeführt werden soll. Wir werden in diesem Fall einen Mock für unser unerwünschtes Objekt erstellen, der mit einer Ausnahme absolut identisch ist: statt eine cURL-Anfrage durchzuführen liefert er direkt ein Objekt zurück, das wir zuvor definiert haben. So wird die variable und zufällige Seite ausgeschaltet.

Es ist nicht nötig, einen Mock zu testen, da wir ja selbst die Daten definieren, die er zurückliefert. Das wäre so, als würde man testen, ob 1 wirklich gleich 1 ist.
Wenn eine getestete Methode jedoch auf einem Zwischenobjekt mit variablem Ergebnis beruht (im Allgemeinen etwas, was ein von außen übernommenes Ergebnis zurückliefert), ist es äußerst wichtig, für dieses Zwischenobjekt einen Mock zu erstellen.

Fazit

Wie wir sehen konnten, sind Modultests sowohl ein sehr umfassendes Thema (ich habe mich auf die Basisprinzipien beschränkt). Sie sind langwierig, aber auch sehr wichtig, um ein Projekt sicher durchzuführen und zu flankieren. Sie ermöglichen es, die kommenden Entwicklungen zu überwachen und sind wie eine Sicherung zum Schutz gegen Regressionen und zweckentfremdete und risikobehaftete Benutzungen unserer Anwendungen und Websites.

Wenn Sie also das nächste Mal ein Projekt realisieren, vergessen Sie nicht, sich gut abzusichern und nehmen Sie sich ein wenig Zeit, um einige Tests zu schreiben, vielleicht rettet dies sogar Ihren Einsatz 😉

WAX Interactive
veröffentlicht am von
WAX Interactive
0 Comments

Schreiben Sie einen Kommentar

Ihre E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.