Zum Inhalt
wannacode

Komponententests

Kurz gesagt: Ein Szenario ist für die Beispiele, auf die sich das Geschäft geeinigt hat und die es lesen will. Ein Komponententest ruft den Code direkt auf: zehn Beträge in Millisekunden, ohne Schritte und ohne Gherkin. Beide prüfen eine Regel für verschiedene Leser. Die Szenarien bleiben wenige, und die Komponententests übernehmen die technischen Fälle.

Problem

Fünfzehn Szenarien für eine Grenze

Der Programmierer hat beschlossen, die Lieferung gründlich zu prüfen: 1 Cent, 10 €, 49,49 €, 49,50 €, 49,99 €, 50 €, 50,01 €, 100 €, 250 €, ein negativer Betrag. Er hat alles als Szenarien aufgeschrieben.

Die Inhaberin des Shops hat die Datei mit den Szenarien geöffnet, um die Lieferregel zu sehen. Sie sah fünfzehn fast gleiche Szenarien und schloss die Datei. Die lebende Dokumentation wurde nicht mehr gelesen.

Die Idee

Der Test ruft den Code direkt auf

Aus dem Programmierkurs kennst du Komponententests: eine Methode, die den Code aufruft und das Ergebnis vergleicht. Die Lieferregel braucht weder Gherkin noch Schritte. Der Test ruft die Methode direkt auf:

src/test/java/shop/DeliveryTest.java
@ParameterizedTest(name = "feeFor({0}) = {1}")
@CsvSource({ "4999, 490", "5000, 0", "1, 490", "25000, 0" })
void feeForTotal(int total, int fee) {
assertEquals(fee, Delivery.feeFor(total));
}

Das ist ein parametrisierter Test: eine Prüfung und eine Tabelle von Fällen. Er sieht aus wie ein Szenariogrundriss mit „Beispiele“, aber er ist für den Programmierer. Ein neuer Fall ist eine neue Zeile in der Tabelle.

Die Ablehnung wird mit einem eigenen Test geprüft. Die Regel wirft eine Exception, und der Test prüft, dass sie es tut:

src/test/java/shop/DeliveryTest.java
@Test
@DisplayName("ein leerer Warenkorb wird abgelehnt")
void emptyCartIsRefused() {
assertThrows(IllegalArgumentException.class, () -> Delivery.feeFor(0));
}
Genauer, für Neugierige

Die Werkzeuge sind die Standardwerkzeuge jeder Sprache: JUnit 5 (parametrisierte Tests leben im Modul junit-jupiter-params), xUnit mit [Theory] und [InlineData], pytest und node:test, in Node eingebaut. Im Programmierkurs nutzten die Python-Tests unittest. pytest ist das verbreitetere: Ein Test ist eine gewöhnliche Funktion mit assert, und parametrize baut die Tabelle.

Die Idee

Welche Regel auf welcher Stufe

Die Regel ist eine. Die Prüfungen liegen auf zwei Stufen, und jede hat ihren eigenen Leser:

  • Ein Szenario ist für die Beispiele, auf die sich das Geschäft geeinigt hat: genau 50,00 €, 49,99 €, ein leerer Warenkorb. Ein oder zwei Beispiele pro Regel. Es ist die lebende Dokumentation, und das Geschäft liest sie.
  • Ein Komponententest ist für die technischen Fälle: 1 Cent, ein großer Betrag, ein negativer Betrag, Rundung. Sie schützen den Code, aber sie sagen nichts Neues über die Regel.

Die Grenze auf beiden Stufen zu prüfen ist keine Verschwendung. Das Szenario sagt, was vereinbart wurde. Der Komponententest schlägt zuerst fehl, und zwar genau dort, wo der Fehler ist.

Die Regel für die Wahl ist kurz: Würde das Geschäft dieses Beispiel lesen wollen? Wenn ja, ein Szenario. Wenn nicht, ein Komponententest.

Probier es aus

Szenario oder Komponententest?

1. „Ein Warenkorb von genau 50,00 €: Die Lieferung ist kostenlos.“ Das Geschäft hat sich beim Story-Treffen darauf geeinigt.
2. Ein Betrag von 1 Cent, ein Betrag von 250 € und ein Betrag von 99.999,99 €.
3. Ein negativer Betrag, sagen wir -5,00 €, den kein Kunde eingeben kann.

Die Idee

Der Lauf

Die ganze Testdatei, ausgeführt gegen den richtigen Code. Alles besteht:

src/test/java/shop/DeliveryTest.java
package shop;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class DeliveryTest {
@ParameterizedTest(name = "feeFor({0}) = {1}")
@CsvSource({ "4999, 490", "5000, 0", "1, 490", "25000, 0" })
void feeForTotal(int total, int fee) {
assertEquals(fee, Delivery.feeFor(total));
}
@Test
@DisplayName("ein leerer Warenkorb wird abgelehnt")
void emptyCartIsRefused() {
assertThrows(IllegalArgumentException.class, () -> Delivery.feeFor(0));
}
}
Ausgabe
OK ein leerer Warenkorb wird abgelehnt
OK feeFor(1) = 490
OK feeFor(25000) = 0
OK feeFor(4999) = 490
OK feeFor(5000) = 0
Tests: 5, bestanden: 5, fehlgeschlagen: 0

Jetzt „vereinfacht“ der Programmierer den Vergleich und rundet den Betrag davor auf ganze Euro. Aus 49,99 € werden 50 €, und die Lieferung wird kostenlos:

src/main/java/shop/Delivery.java
package shop;
public class Delivery {
public static final int FREE_FROM = 5000;
public static final int FEE = 490;
// Die Liefergebühr in Cent. total ist der Warenkorbwert, ebenfalls in Cent.
public static int feeFor(int total) {
if (total <= 0) {
throw new IllegalArgumentException("Ein leerer Warenkorb kann nicht bestellt werden");
}
return Math.round(total / 100.0) * 100 >= FREE_FROM ? 0 : FEE;
}
}

Dieselben Tests, gegen den neuen Code:

src/test/java/shop/DeliveryTest.java
package shop;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class DeliveryTest {
@ParameterizedTest(name = "feeFor({0}) = {1}")
@CsvSource({ "4999, 490", "5000, 0", "1, 490", "25000, 0" })
void feeForTotal(int total, int fee) {
assertEquals(fee, Delivery.feeFor(total));
}
@Test
@DisplayName("ein leerer Warenkorb wird abgelehnt")
void emptyCartIsRefused() {
assertThrows(IllegalArgumentException.class, () -> Delivery.feeFor(0));
}
}
Ausgabe
FEHLER feeFor(4999) = 490: expected: <490> but was: <0>
OK ein leerer Warenkorb wird abgelehnt
OK feeFor(1) = 490
OK feeFor(25000) = 0
OK feeFor(5000) = 0
Tests: 5, bestanden: 4, fehlgeschlagen: 1

Genau ein Fall schlägt fehl: 49,99 €. Der Name des Tests sagt dir den Betrag, und die Meldung sagt dir den erwarteten und den tatsächlichen Wert. Du musst nicht suchen, welcher Schritt welches Szenarios es war.

Genauer, für Neugierige

Die Ausgabe hier ist ein kurzer Bericht, nach Namen geordnet: Die Werkzeuge laufen wirklich, aber in einer Hülle, die nur die Zeile für jeden Test und die Anzahl ausgibt. Die Namen der Tests unterscheiden sich zwischen den Sprachen, weil jedes Werkzeug sie auf seine Art baut: JUnit aus dem name des parametrisierten Tests, xUnit aus dem Methodennamen und den Argumenten, pytest aus dem Funktionsnamen und den Werten, node:test aus dem Text, den du angibst.

Die Idee

Warum nicht alles in Gherkin

Gherkin ist für das Gespräch mit dem Geschäft: die Beispiele, auf die sich die Three Amigos geeinigt haben, so geschrieben, dass sie jeder lesen kann. Als Testwerkzeug ist es teuer:

  • Jede neue Zeile in einem Szenario braucht einen Schritt, und Schritte müssen gepflegt werden;
  • ein Szenario geht durch mehr Code, ist also langsamer;
  • wenn es fehlschlägt, ist die Ursache weiter von der Meldung entfernt.

Und vor allem: Fünfzehn technische Szenarien verstecken die drei, die die Regel aussagen. Lebende Dokumentation bleibt nur lebendig, solange sie jemand liest.

Also sind die Szenarien wenige und sagen die Regel aus. Die Komponententests sind viele und schützen den Code. Das ist auch die Pyramide: eine breite Basis schneller Tests und darüber weniger, langsamere, die dem Kunden näher sind.

Genauer, für Neugierige

Aslak Hellesøy, der Cucumber geschaffen hat, hat mehr als einmal geschrieben, dass Cucumber vor allem ein Werkzeug für eine gemeinsame Sprache und Zusammenarbeit ist und die Tests ein Nebenprodukt sind. Wenn die Szenarien nicht gemeinsam mit dem Geschäft geschrieben und gelesen werden, bleibt ein teurer Weg, Tests zu schreiben.

Übung

Die zwei Stufen

1. Welcher Test schützt was. Die Fragen gehen um die Szenarien aus der vorigen Lektion und die Tests aus dieser.

1. Du entfernst den Fall 250,00 € (25000 Cent) aus dem parametrisierten Test. Welchen Fehler fängt dann kein Test der Lektion mehr?
2. Welcher Test fängt den Fehler „ein leerer Warenkorb wird nicht abgelehnt, sondern zahlt 4,90 €“?
3. Der Programmierer will 49,50 € sowohl zu den Szenarien als auch zum Komponententest hinzufügen. Was sagst du?

Test

Teste dich

1. Was ist der Unterschied zwischen einem Szenario und einem Komponententest für eine Regel?
2. Warum ist ein Komponententest schneller als ein Szenario?
3. Was macht ein parametrisierter Test?
4. Warum wird nicht alles in Gherkin geschrieben?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·