Moderne Best Practices für Java-Tests

Dieser Artikel ist eine japanische Übersetzung von Modern Best Practices zum Testen in Java. Übersetzt und veröffentlicht mit Genehmigung des Autors des Originalartikels. Ich bin neu in der Übersetzung, daher denke ich, dass es einige seltsame Dinge gibt, aber bitte vergib mir.

Der gleiche Artikel wird im Hatena-Blog veröffentlicht. https://dhirabayashi.hatenablog.com/entry/2020/04/21/190009


Einfach zu wartender und leicht zu lesender Testcode ist wichtig, um eine gute Testabdeckung zu erreichen, mit der Sie neue Funktionen implementieren und umgestalten können, ohne befürchten zu müssen, dass etwas kaputt geht. Dieser Artikel enthält viele Best Practices, die ich im Laufe der Jahre beim Schreiben von Unit- und Integrationstests in Java erworben habe. Dazu gehören auch moderne Technologien wie JUnit 5, AssertJ, Testcontainer und Kotlin. Einige scheinen offensichtlich zu sein, andere sind möglicherweise nicht mit dem kompatibel, was Sie in Büchern über Softwareentwicklung und -tests gelesen haben.

TL;DR

Basic

Given, When, Then Der Test sollte aus drei Blöcken bestehen, die durch eine Leerzeile getrennt sind. Jeder Codeblock sollte so kurz wie möglich sein. Verwenden wir Unterfunktionen, um den Block zu verkürzen.

//Gutes Beispiel
@Test
public void findProduct() {
    insertIntoDatabase(new Product(100, "Smartphone"));

    Product product = dao.findProduct(100);

    assertThat(product.getName()).isEqualTo("Smartphone");
}

Verwenden Sie die Präfixe "Ist *" und "Erwartet *"

//schlechtes Beispiel
ProductDTO product1 = requestProduct(1);

ProductDTO product2 = new ProductDTO("1", List.of(State.ACTIVE, State.REJECTED))
assertThat(product1).isEqualTo(product2);

Wenn Sie Variablen in derselben Wertzusicherung verwenden möchten, stellen Sie dem Variablennamen "tatsächlich" oder "erwartet" voran. Dies erleichtert das Lesen und macht die Absicht der Variablen klar. Darüber hinaus wird das Risiko einer Verwechslung von erwarteten und gemessenen Werten verringert.

//Gutes Beispiel
ProductDTO actualProduct = requestProduct(1);

ProductDTO expectedProduct = new ProductDTO("1", List.of(State.ACTIVE, State.REJECTED))
assertThat(actualProduct).isEqualTo(expectedProduct); //Großartig und klar

Verwenden Sie feste Werte anstelle von zufälligen Werten

Vermeiden Sie zufällige Werte, da diese Ihre Tests instabil und schwer zu debuggen machen, Fehlermeldungen weglassen und es schwierig machen können, Fehler in Ihrem Code zu verfolgen.

//schlechtes Beispiel
Instant ts1 = Instant.now(); // 1557582788
Instant ts2 = ts1.plusSeconds(1); // 1557582789
int randomAmount = new Random().nextInt(500); // 232
UUID uuid = UUID.randomUUID(); // d5d1f61b-0a8b-42be-b05a-bd458bb563ad

Verwenden Sie stattdessen für alles feste Werte. Feste Werte geben Fehlermeldungen aus, die den Test reproduzierbarer, leichter zu debuggen und leichter zu den relevanten Codezeilen zu verfolgen machen.

//Gutes Beispiel
Instant ts1 = Instant.ofEpochSecond(1550000001);
Instant ts2 = Instant.ofEpochSecond(1550000002);
int amount = 50;
UUID uuid = UUID.fromString("00000000-000-0000-0000-000000000001");

Sie können die Eingabe reduzieren, indem Sie [Hilfsfunktion verwenden] verwenden (verwenden Sie häufig die Hilfsfunktion #).

Schreiben Sie einen kleinen und klaren Test

Helferfunktionen stark nutzen

Extrahieren Sie kleinen oder wiederkehrenden Code in Unterfunktionen und geben Sie ihnen beschreibende Namen. Es ist insofern leistungsstark, als es den Test kurz hält und es einfach macht, den Punkt des Tests auf einen Blick zu erfassen.

//schlechtes Beispiel
@Test
public void categoryQueryParameter() throws Exception {
    List<ProductEntity> products = List.of(
            new ProductEntity().setId("1").setName("Envelope").setCategory("Office").setDescription("An Envelope").setStockAmount(1),
            new ProductEntity().setId("2").setName("Pen").setCategory("Office").setDescription("A Pen").setStockAmount(1),
            new ProductEntity().setId("3").setName("Notebook").setCategory("Hardware").setDescription("A Notebook").setStockAmount(2)
    );
    for (ProductEntity product : products) {
        template.execute(createSqlInsertStatement(product));
    }

    String responseJson = client.perform(get("/products?category=Office"))
            .andExpect(status().is(200))
            .andReturn().getResponse().getContentAsString();

    assertThat(toDTOs(responseJson))
            .extracting(ProductDTO::getId)
            .containsOnly("1", "2");
}
//Gutes Beispiel
@Test
public void categoryQueryParameter2() throws Exception {
    insertIntoDatabase(
            createProductWithCategory("1", "Office"),
            createProductWithCategory("2", "Office"),
            createProductWithCategory("3", "Hardware")
    );

    String responseJson = requestProductsByCategory("Office");

    assertThat(toDTOs(responseJson))
            .extracting(ProductDTO::getId)
            .containsOnly("1", "2");
}
//Gutes Beispiel(Java)
Instant ts = toInstant(1); // Instant.ofEpochSecond(1550000001)
UUID id = toUUID(1); // UUID.fromString("00000000-0000-0000-a000-000000000001")
//Gutes Beispiel(Kotlin)
val ts = 1.toInstant()
val id = 1.toUUID()

Diese Hilfsfunktion wird in Kotlin folgendermaßen implementiert:

fun Int.toInstant(): Instant = Instant.ofEpochSecond(this.toLong())

fun Int.toUUID(): UUID = UUID.fromString("00000000-0000-0000-a000-${this.toString().padStart(11, '0')}")

Variablen nicht überbeanspruchen

Das Extrahieren von Werten, die mehrfach verwendet werden, in Variablen ist eine gängige Praxis für Entwickler.

//schlechtes Beispiel
@Test
public void variables() throws Exception {
    String relevantCategory = "Office";
    String id1 = "4243";
    String id2 = "1123";
    String id3 = "9213";
    String irrelevantCategory = "Hardware";
    insertIntoDatabase(
            createProductWithCategory(id1, relevantCategory),
            createProductWithCategory(id2, relevantCategory),
            createProductWithCategory(id3, irrelevantCategory)
    );

    String responseJson = requestProductsByCategory(relevantCategory);

    assertThat(toDTOs(responseJson))
            .extracting(ProductDTO::getId)
            .containsOnly(id1, id2);
}

Leider erhöht dies den Testcode erheblich. Darüber hinaus ist es schwierig, den Wert aus der resultierenden Testfehlermeldung bis zur relevanten Codezeile zurückzuverfolgen.

KISS-Prinzipien> DRY-Prinzipien

//Gutes Beispiel
@Test
public void variables() throws Exception {
    insertIntoDatabase(
            createProductWithCategory("4243", "Office"),
            createProductWithCategory("1123", "Office"),
            createProductWithCategory("9213", "Hardware")
    );

    String responseJson = requestProductsByCategory("Office");

    assertThat(toDTOs(responseJson))
            .extracting(ProductDTO::getId)
            .containsOnly("4243", "1123");
}

Wenn der Testcode kurz gehalten wird (es wird dringend empfohlen), ist es nichts Falsches zu wissen, wo der gleiche Wert verwendet wird. Darüber hinaus sind die Methoden noch kürzer und daher leichter zu verstehen. Und schließlich erleichtert die Fehlermeldung in diesem Fall das Zurückverfolgen des Codes.

Erweitern Sie vorhandene Tests nicht auf "nur noch eine kleine Sache testen".

//schlechtes Beispiel
public class ProductControllerTest {
    @Test
    public void happyPath() {
        //Viel Code hier...
    }
}

Das Hinzufügen von Tests für seltene Fälle zu bestehenden Tests (von Happypath [^ happy_path]) ist faszinierend. [^ happy_path]: [Übersetzung] Es scheint so etwas wie ein normales System zu bedeuten. Der Test ist jedoch groß und schwer zu verstehen. Es macht es schwierig, alle relevanten Testfälle zu verfolgen, die von diesem großen Test abgedeckt werden. Diese Tests werden üblicherweise als "Happy Path Tests" bezeichnet [^ happy_path_test]. [^ happy_path_test]: [Übersetzung] Hier werden die Größe eines Tests und die Schwierigkeit, den Inhalt zu verstehen, als Probleme angesehen, aber selbst wenn Sie nachschlagen, scheint das Wort "Happy Path Test" keine solche Bedeutung zu haben. Und ich weiß ehrlich gesagt nicht, was es bedeutet. Selbst wenn ich die Bedeutung dieses Satzes nicht verstehe, tut es nicht so weh, also lasse ich es so wie es ist. Wenn ein solcher Test fehlschlägt, ist es schwierig, genau zu wissen, was schief gelaufen ist.

//Gutes Beispiel
public class ProductControllerTest {
    @Test
    public void multipleProductsAreReturned() {}
    @Test
    public void allProductValuesAreReturned() {}
    @Test
    public void filterByCategory() {}
    @Test
    public void filterByDateCreated() {}
}

Erstellen Sie stattdessen eine neue Testmethode mit einem beschreibenden Namen, der Ihnen alles über das erwartete Verhalten sagt. Ja, Sie schreiben mehr, aber Sie können einen eindeutigen Test erstellen, der Ihrem Zweck entspricht, und nur das relevante Verhalten testen. Auch hier reduzieren Hilfsfunktionen die Eingabe. Und schließlich ist das Hinzufügen eines speziell erstellten Tests mit einem beschreibenden Namen eine hervorragende Möglichkeit, das implementierte Verhalten aufzuzeichnen.

Geben Sie nur an, was Sie testen möchten

Überlegen Sie, was Sie wirklich testen möchten. Vermeiden Sie es, mehr zu behaupten, als Sie brauchen, nur weil Sie es können. Denken Sie außerdem daran, was Sie bereits im vorherigen Test getestet haben. Normalerweise müssen Sie nicht bei jedem Test mehrmals dasselbe behaupten. Dies hält den Test kurz, klar und lenkt nicht vom erwarteten Verhalten ab.

Betrachten Sie ein Beispiel. Testen Sie auf HTTP-Endpunkte, die Produktinformationen zurückgeben. Die Testsuite sollte die folgenden Tests enthalten:

  1. Ein großer "Zuordnungstest", der bestätigt, dass alle aus der Datenbank abgerufenen Werte korrekt als korrekt zugeordnete JSON-Nutzdaten im richtigen Format zurückgegeben werden. Wenn equals () korrekt implementiert ist, können Sie es einfach mit AssertJs isEqualTo () (für ein einzelnes Element) oder includesOnly () (für mehrere Elemente) bestätigen.
String responseJson = requestProducts();

ProductDTO expectedDTO1 = new ProductDTO("1", "evelope", new Category("office"), List.of(States.ACTIVE, States.REJECTED));
ProductDTO expectedDTO2 = new ProductDTO("2", "evelope", new Category("smartphone"), List.of(States.ACTIVE));
assertThat(toDTOs(responseJson))
        .containsOnly(expectedDTO1, expectedDTO2);
  1. Ein Test, der das korrekte Verhalten des Abfrageparameters "? Category" überprüft. Wir möchten testen, ob es richtig gefiltert wird, nicht ob alle Eigenschaften richtig eingestellt sind. Im obigen Fall wurde es bereits getestet. Daher ist es ausreichend, nur die zurückgegebenen Produkt-IDs zu vergleichen.
String responseJson = requestProductsByCategory("Office");

assertThat(toDTOs(responseJson))
        .extracting(ProductDTO::getId)
        .containsOnly("1", "2");
  1. Ein Test, der nach seltenen Fällen oder spezieller Geschäftslogik sucht. Wird beispielsweise ein bestimmter Wert in der Nutzlast korrekt berechnet? In diesem Fall interessieren mich nur bestimmte JSON-Felder in der Nutzlast. Daher sollten nur relevante Felder überprüft werden, um den Umfang der zu testenden Logik zu verdeutlichen und zu dokumentieren. Auch hier müssen Sie nicht alle Felder erneut aktivieren, da dies hier keine Rolle spielt.
assertThat(actualProduct.getPrice()).isEqualTo(100);

In sich geschlossener Test

Verwandte Parameter nicht ausblenden (in der Hilfsfunktion)

//schlechtes Beispiel
insertIntoDatabase(createProduct());
List<ProductDTO> actualProducts = requestProductsByCategory();
assertThat(actualProducts).containsOnly(new ProductDTO("1", "Office"));

Ja, Sie sollten Hilfsfunktionen für die Datengenerierung und Zusicherungen verwenden. Aber Sie müssen sie parametrisieren. Definieren Sie Parameter für alles, was für Ihren Test wichtig ist und von Ihrem Test gesteuert werden muss. Zwingen Sie die Leser der Quelle nicht, zur Funktionsdefinition zu springen, um den Testinhalt zu verstehen. Empirische Regel: Sie sollten in der Lage sein, die Hauptpunkte des Tests zu verstehen, indem Sie nur die Testmethode betrachten.

//Gutes Beispiel
insertIntoDatabase(createProduct("1", "Office"));
List<ProductDTO> actualProducts = requestProductsByCategory("Office");
assertThat(actualProducts).containsOnly(new ProductDTO("1", "Office"));

Fügen Sie die Daten korrekt in die Testmethode ein

Alles in der Testmethode muss korrekt sein. Das Verschieben des wiederverwendbaren Dateneinfügecodes in die Methode "@ Before" ist faszinierend, aber dann muss der Quellleser herumfliegen, um vollständig zu verstehen, was der Test tut. Wird nicht sein. Auch hier helfen Hilfsfunktionen, die Daten einfügen, diese sich wiederholende Aufgabe in eine Zeile zu setzen.

Ziehen Sie die Komposition der Vererbung vor

Erstellen Sie in Ihren Testklassen keine komplexen Vererbungshierarchien.

//schlechtes Beispiel
class SimpleBaseTest {}
class AdvancedBaseTest extends SimpleBaseTest {}
class AllInklusiveBaseTest extends AdvancedBaseTest {}
class MyTest extends AllInklusiveBaseTest {}

Eine solche Hierarchie macht es schwierig zu verstehen, und es ist wahrscheinlich, dass Sie eine Basistestklasse erben, die viele Dinge enthält, die Sie für Ihren aktuellen Test nicht benötigen. Dies lenkt den Leser des Codes ab und kann Fehler verursachen. Vererbung ist nicht flexibel: Es ist nicht möglich, alles zu verwenden, was von "AllInklusiveBaseTest" erbt, aber gibt es etwas, das von dieser Oberklasse "AdvancedBaseTest" erbt? [^ Vererbung] Außerdem müssen Codeleser zwischen mehreren Basisklassen wechseln, um das Gesamtbild zu erhalten. [^ Vererbung]: Ich habe die Bedeutung nicht verstanden, aber es ist schwierig zu unterscheiden, ob die von "AllInklusiveBaseTest" geerbten Mitglieder von "AllInklusiveBaseTest" oder "AdvancedBaseTest" abgeleitet sind. Ich gehe davon aus, dass es schwierig ist zu verstehen, was von wo geerbt wird, wenn die Vererbungshierarchie tief ist. Es besteht jedoch die Möglichkeit, dass der Satz aufgrund einer Fehlübersetzung seltsam ist ...

"Duplikate sind besser als falsche Abstraktionen" RDX in 10 Modern Software Over-Engineering Mistakes

Wir empfehlen, stattdessen die Komposition zu verwenden. Schreiben Sie ein kleines Code-Snippet und eine Klasse für jede bestimmte Fixture-Aufgabe (Testdatenbank starten, Schema generieren, Daten einfügen, Mock-Webserver starten). Lassen Sie uns diese Teile in der Methode mit @ BeforeAll wiederverwenden oder indem wir das generierte Objekt dem Feld der Testklasse zuweisen. Sie erstellen also alle neuen Testklassen, indem Sie diese Teile wiederverwenden. Genau wie ein Legoblock. Auf diese Weise haben alle Tests eine Vorrichtung, die perfekt zu Ihnen passt, leicht zu verstehen ist und nichts mit Ereignissen zu tun hat. Die Testklasse ist in sich geschlossen, da alle Zuordnungen innerhalb der Testklasse korrekt sind.

//Gutes Beispiel
public class MyTest {
    //Zusammensetzung statt Vererbung
    private JdbcTemplate template;
    private MockWebServer taxService;

    @BeforeAll
    public void setupDatabaseSchemaAndMockWebServer() throws IOException {
        this.template = new DatabaseFixture().startDatabaseAndCreateSchema();
        this.taxService = new MockWebServer();
        taxService.start();
    }
}

//Eine andere Datei
public class DatabaseFixture {
    public JdbcTemplate startDatabaseAndCreateSchema() throws IOException {
        PostgreSQLContainer db = new PostgreSQLContainer("postgres:11.2-alpine");
        db.start();
        DataSource dataSource = DataSourceBuilder.create()
                .driverClassName("org.postgresql.Driver")
                .username(db.getUsername())
                .password(db.getPassword())
                .url(db.getJdbcUrl())
                .build();
        JdbcTemplate template = new JdbcTemplate(dataSource);
        SchemaCreator.createSchema(template);
        return template;
    }
}

Wiederholen:

KISS-Prinzipien> DRY-Prinzipien

Der Dump-Test ist großartig: Vergleichen Sie die Ausgabe mit fest codierten Werten

Produktionscode nicht wiederverwenden

Der Test sollte den Produktionscode testen: anstatt den Produktionscode wiederzuverwenden. Wenn Sie Produktionscode in Ihren Tests wiederverwenden, können Fehler aufgrund des wiederverwendeten Codes übersehen werden, da der Code nicht mehr getestet wird [^ reuse_production].

//schlechtes Beispiel
boolean isActive = true;
boolean isRejected = true;
insertIntoDatabase(new Product(1, isActive, isRejected));

ProductDTO actualDTO = requestProduct(1);

//Wiederverwendung von Produktionscode
List<State> expectedStates = ProductionCode.mapBooleansToEnumList(isActive, isRejected);
assertThat(actualDTO.states).isEqualTo(expectedStates);

[^ reuse_production]: Hier verwenden wir den Produktionscode, um die erwarteten Werte zu generieren. Da wir den Produktionscode sowohl für die Generierung von Erwartungswerten als auch für die Generierung von Messwerten verwenden, wird davon ausgegangen, dass er praktisch nichts testet.

Denken Sie beim Schreiben eines Tests stattdessen an Eingabe und Ausgabe. Der Test stellt den Eingabewert ein und vergleicht den tatsächlichen Ausgabewert mit dem fest codierten Wert. In den meisten Fällen müssen Sie Ihren Code nicht wiederverwenden.

//Gutes Beispiel
assertThat(actualDTO.states).isEqualTo(List.of(States.ACTIVE, States.REJECTED));

Schreiben Sie nicht die gleiche Logik wie der Produktionscode im Test

Der Mapping-Code ist ein häufiges Beispiel für die Neuerfindung von Logik in einem Test. Angenommen, unser Test enthält eine "mapEntityToDto ()" - Methode, die einen DTO-Rückgabewert zurückgibt, der verwendet wird, um zu bestätigen, dass er denselben Wert enthält wie die zu Beginn des Tests eingefügte Entität. In diesem Fall schreiben Sie wahrscheinlich dieselbe Logik in Ihren Testcode wie in Ihren Produktionscode. Es kann Fehler enthalten.

//schlechtes Beispiel
ProductEntity inputEntity = new ProductEntity(1, "evelope", "office", false, true, 200, 10.0);
insertIntoDatabase(input);

ProductDTO actualDTO = requestProduct(1);

 // mapEntityToDto()Enthält dieselbe Zuordnungslogik wie der Produktionscode
ProductDTO expectedDTO = mapEntityToDto(inputEntity);
assertThat(actualDTO).isEqualTo(expectedDTO);

Die Lösung besteht wiederum darin, das "actualDTO" mit einem manuell generierten Referenzobjekt zu vergleichen, das einen fest codierten Wert enthält. Es ist sehr einfach, leicht zu verstehen und fehlerfrei.

//Gutes Beispiel
ProductDTO expectedDTO = new ProductDTO("1", "evelope", new Category("office"), List.of(States.ACTIVE, States.REJECTED))
assertThat(actualDTO).isEqualTo(expectedDTO);

Wenn Sie nicht alle Werte vergleichen und daher kein vollständiges Referenzobjekt generieren möchten, sollten Sie nur die Unterobjekte oder nur die zugehörigen Werte vergleichen.

Schreibe nicht zu viel Logik

Auch hier geht es beim Testen hauptsächlich um Ein- und Ausgänge. Es dient zur Eingabe und zum Vergleich von gemessenen und erwarteten Werten. Daher schreiben Sie nicht und sollten nicht zu viel Logik in Ihre Tests schreiben. Wenn Sie Logik mit vielen Schleifen und Bedingungen implementieren, ist der Test schwerer zu verstehen und fehleranfälliger. Bei komplexer Assertionslogik können die leistungsstarken AssertJ-Assertions die harte Arbeit für Sie erledigen. [^ too_much_logic] [^ too_much_logic]: AssertJ erledigt das für Sie, sodass Sie nicht hart arbeiten müssen, um die Logik zu schreiben.

Realistischer Test

Konzentrieren Sie sich darauf, vollständige vertikale Objektträger zu testen

Im Allgemeinen wird empfohlen, ein Modell zu verwenden, um jede Klasse einzeln zu testen. Aber es hat schwerwiegende Nachteile: Sie alle Internes Refactoring zerstört alle Tests, da wir die Klassen nicht auf integrierte Weise testen, sondern jede interne Klasse einen Test hat. Und schließlich müssen Sie verschiedene Tests schreiben und pflegen. Screen Shot 2020-04-10 at 23.17.05.png

Stattdessen empfehlen wir Focus on Integration Tests (https://phauer.com/2019/focus-integration-tests-mock-based-tests/#integration-tests). Ein "Integrationstest" ist ein Test, der alle Klassen (wie Produktionscode) zusammenfasst und alle technischen Ebenen (HTTP, Geschäftslogik, Datenbank) auf einer perfekten vertikalen Folie durchläuft. Diese Methode testet eher das Verhalten als die Implementierung. Diese Tests sind genau, produktionsnah und robust gegen internes Refactoring. Im Idealfall müssen Sie nur einen Test schreiben.

Screen Shot 2020-04-11 at 22.51.32.png * Es wird empfohlen, auf den Integrationstest zu achten (= das reale Objekt zusammenschreiben und alles auf einmal testen) *

Zu diesem Thema gibt es noch viel mehr zu sagen. Weitere Informationen finden Sie in meinem Blog-Beitrag "Fokus auf Integrationstests anstelle von Mock-basierten Tests". ..

Verwenden Sie keine In-Memory-Datenbank zum Testen

Screen Shot 2020-04-11 at 22.57.41.png * Wenn Sie eine In-Memory-Datenbank verwenden, testen Sie anhand einer Datenbank, die sich von der Produktionsumgebung unterscheidet. *

Verwenden Sie zum Testen eine In-Memory-Datenbank (H2, HSQLDB, [Fongo](https :: //github.com/fakemongo/fongo)) reduziert die Zuverlässigkeit und den Testumfang. Die In-Memory-Datenbank und die in der Produktionsumgebung verwendete Datenbank können sich unterschiedlich verhalten und unterschiedliche Ergebnisse zurückgeben. Das Testen auf der Grundlage einer unreifen In-Memory-Datenbank garantiert daher nicht das korrekte Verhalten von Produktionsanwendungen. Darüber hinaus können Sie leicht auf Situationen stoßen, in denen Sie bestimmte (datenbankspezifische) Funktionen nicht verwenden (oder testen) können. Dies liegt daran, dass In-Memory-Datenbanken nicht unterstützen oder sich anders verhalten. Weitere Informationen hierzu finden Sie im Artikel Verwenden Sie keine In-Memory-Datenbanken für Tests. Bitte.

Die Lösung besteht darin, einen Test für die tatsächliche Datenbank auszuführen. Glücklicherweise bietet eine Bibliothek namens Testcontainers eine großartige Java-API zum Verwalten von Containern direkt in Ihrem Testcode. Informationen zur Beschleunigung der Ausführung finden Sie hier (https://phauer.com/2019/focus-integration-tests-mock-based-tests/#execution-speed).

Java/JVM

Verwenden Sie -noverify -XX: TieredStopAtLevel = 1

Fügen Sie Ihren Ausführungseinstellungen immer die JVM-Option -noverify -XX: TieredStopAtLevel = 1 hinzu. Dies spart 1-2 Sekunden bei der JVM-Startzeit, bevor der Test ausgeführt wird. Dies ist besonders nützlich bei der ersten Entwicklung von Tests, bei denen Sie Tests häufig über die IDE ausführen.

Update: Ab Java 13 ist -noverify veraltet. [^ nooverify] [^ nooverify]: Dieses "Update" ist eine originalgetreue Übersetzung von "Update" im Originalartikel und war ursprünglich im Originalartikel enthalten. Dies ist kein Update der übersetzten Version, die Sie lesen.

Tipp: Mit IntelliJ IDEA können Sie dieses Argument zur Startkonfigurationsvorlage "JUnit" hinzufügen, sodass Sie nicht für jede neue Ausführungskonfiguration ein Argument hinzufügen müssen.

screenshot-idea-run-config-template-default-vm-options-marked.png

Verwenden Sie AssertJ

AssertJ ist eine sehr fließende sichere API vom Typ [^ fließend] mit einer Vielzahl von Zusicherungen und beschreibenden Fehlermeldungen. Eine leistungsstarke und ausgereifte Assertionsbibliothek. [^ fließend]: fließend. Dies bedeutet, dass Sie in eine Methodenkette schreiben können. Alle gewünschten Aussagen sind hier. Dies hält Ihren Testcode kurz und vermeidet gleichzeitig die Notwendigkeit, komplexe Assertionslogik mit Schleifen und Bedingungen zu schreiben. Hier sind einige Beispiele:

assertThat(actualProduct)
        .isEqualToIgnoringGivenFields(expectedProduct, "id");

assertThat(actualProductList).containsExactly(
        createProductDTO("1", "Smartphone", 250.00),
        createProductDTO("1", "Smartphone", 250.00)
);

assertThat(actualProductList)
        .usingElementComparatorIgnoringFields("id")
        .containsExactly(expectedProduct1, expectedProduct2);

assertThat(actualProductList)
        .extracting(Product::getId)
        .containsExactly("1", "2");

assertThat(actualProductList)
        .anySatisfy(product -> assertThat(product.getDateCreated()).isBetween(instant1, instant2));

assertThat(actualProductList)
        .filteredOn(product -> product.getCategory().equals("Smartphone"))
        .allSatisfy(product -> assertThat(product.isLiked()).isTrue());

Vermeiden Sie "assertTrue ()" und "assertFalse ()"

Vermeiden Sie einfache "assertTrue ()" - oder "assertFalse ()" - Behauptungen, da diese eine mysteriöse Fehlermeldung ausgeben:

//schlechtes Beispiel
assertTrue(actualProductList.contains(expectedProduct));
assertTrue(actualProductList.size() == 5);
assertTrue(actualProduct instanceof Product);
expected: <true> but was: <false>

Verwenden Sie stattdessen die AssertJ-Assertion, die eine gute Fehlermeldung [^ out_of_the_box] ohne spezielle Anpassung ausgibt.

//Gutes Beispiel
assertThat(actualProductList).contains(expectedProduct);
assertThat(actualProductList).hasSize(5);
assertThat(actualProduct).isInstanceOf(Product.class);
Expecting:
 <[Product[id=1, name='Samsung Galaxy']]>
to contain:
 <[Product[id=2, name='iPhone']]>
but could not find:
 <[Product[id=2, name='iPhone']]>

Wenn Sie wirklich gegen den Booleschen Wert prüfen müssen, AssertJ's as (), um die Fehlermeldung zu verbessern Erwägen Sie die Verwendung von features-Highlight.html. [^ out_of_the_box]: Der Originaltext ist "out-of-the-box", was bedeutet, dass Sie ihn mit den Standardeinstellungen verwenden können. Wie Sie später sehen werden, können Sie mit AssertJ die Fehlermeldung mithilfe der as () -Methode anpassen. Ich denke jedoch, dass Sie eine Fehlermeldung erhalten, die Sie nicht ausführen müssen.

Verwenden Sie JUnit 5

JUnit5 ist eine hochmoderne Technologie für (Unit-) Tests. Es wird aktiv entwickelt und bietet viele leistungsstarke Funktionen (wie parametrisierte Tests, Gruppierung, bedingte Tests, Lebenszykluskontrolle).

Verwenden Sie einen parametrisierten Test

Mit parametrisierten Tests kann ein Test mehrmals mit unterschiedlichen Werten ausgeführt werden. Auf diese Weise können Sie problemlos mehrere Fälle testen, ohne einen Testcode hinzuzufügen. JUnit 5 bietet eine großartige Möglichkeit, solche Tests zu schreiben. "@ ValueSource", "@ EnumSource", "@ CsvSource" und "@ MethodSource".

//Gutes Beispiel
@ParameterizedTest
@ValueSource(strings = ["§ed2d", "sdf_", "123123", "§_sdf__dfww!"])
public void rejectedInvalidTokens(String invalidToken) {
    client.perform(get("/products").param("token", invalidToken))
            .andExpect(status().is(400))
}

@ParameterizedTest
@EnumSource(WorkflowState::class, mode = EnumSource.Mode.INCLUDE, names = ["FAILED", "SUCCEEDED"])
public void dontProcessWorkflowInCaseOfAFinalState(WorkflowState itemsInitialState) {
    // ...
}

Ich empfehle dringend, diese ausgiebig zu verwenden. Weil Sie mit minimalem Aufwand mehr Fälle testen können.

Abschließend möchte ich die "@ CsvSource" und "@ MethodSource" hervorheben, die für erweiterte parametrisierte Testszenarien verwendet werden können, in denen Sie auch erwartete Werte mit Parametern steuern können.

@ParameterizedTest
@CsvSource({
    "1, 1, 2",
    "5, 3, 8",
    "10, -20, -10"
})
public void add(int summand1, int summand2, int expectedSum) {
    assertThat(calculator.add(summand1, summand2)).isEqualTo(expectedSum);
}

@ MethodSource ist leistungsstark, wenn es in Kombination mit einem dedizierten Testobjekt verwendet wird, das alle relevanten Testparameter und Erwartungen enthält. Leider ist das Schreiben dieser Datenstrukturen (POJOs) in Java mühsam. Aus diesem Grund verwende ich Kotlins Datenklassen (https://phauer.com/2018/best-practices-unit-testing-kotlin/#utilize-data-classes), um ein Beispiel für diese Funktion unten zu zeigen. ist.

data class TestData(
    val input: String?,
    val expected: Token?
)

@ParameterizedTest
@MethodSource("validTokenProvider")
fun `parse valid tokens`(data: TestData) {
    assertThat(parse(data.input)).isEqualTo(data.expected)
}

private fun validTokenProvider() = Stream.of(
    TestData(input = "1511443755_2", expected = Token(1511443755, "2")),
    TestData(input = "151175_13521", expected = Token(151175, "13521")),
    TestData(input = "151144375_id", expected = Token(151144375, "id")),
    TestData(input = "15114437599_1", expected = Token(15114437599, "1")),
    TestData(input = null, expected = null)
)

Gruppentests

JUnit5s "@ Nested" ist nützlich zum Gruppieren von Testmethoden. Eine sinnvolle Gruppe kann eine bestimmte Art von Test sein (z. B. "InputIsXY", "ErrorCases") oder jede Methode unter einem Test in einer Gruppe. (GetDesign und UpdateDesign)

public class DesignControllerTest {
    @Nested
    class GetDesigns {
        @Test
        void allFieldsAreIncluded() {}
        @Test
        void limitParameter() {}
        @Test
        void filterParameter() {}
    }
    @Nested
    class DeleteDesign {
        @Test
        void designIsRemovedFromDb() {}
        @Test
        void return404OnInvalidIdParameter() {}
        @Test
        void return401IfNotAuthorized() {}
    }
}

screenshot-group-test-methods.png

Einfach zu lesender Testname mit @ DisplayName oder Kotlin Backquote

Java verwendet JUnit5s "@ DisplayName", um einfach zu lesende Testbeschreibungen zu schreiben.

public class DisplayNameTest {
    @Test
    @DisplayName("Design is removed from database")
    void designIsRemoved() {}
    @Test
    @DisplayName("Return 404 in case of an invalid parameter")
    void return404() {}
    @Test
    @DisplayName("Return 401 if the request is not authorized")
    void return401() {}
}

screenshot-displayname.png

In Kotlin können Sie den Methodennamen in das hintere Anführungszeichen schreiben und ein Leerzeichen mit halber Breite einfügen. Dies erleichtert das Lesen und vermeidet Redundanz.

@Test
fun `design is removed from db`() {}

Mock Remote Services

Um den HTTP-Client zu testen, müssen Sie den Remotedienst verspotten. Ich bevorzuge dafür den WebMock-Server von OkHttp (https://github.com/square/okhttp/tree/master/mockwebserver).

MockWebServer serviceMock = new MockWebServer();
serviceMock.start();
HttpUrl baseUrl = serviceMock.url("/v1/");
ProductClient client = new ProductClient(baseUrl.host(), baseUrl.port());
serviceMock.enqueue(new MockResponse()
        .addHeader("Content-Type", "application/json")
        .setBody("{\"name\": \"Smartphone\"}"));

ProductDTO productDTO = client.retrieveProduct("1");

assertThat(productDTO.getName()).isEqualTo("Smartphone");

Verwenden Sie Awaitility für asynchrone Code-Zusicherungen

Awaitility ist eine Bibliothek zum Testen von asynchronem Code. Es ist einfach zu definieren, wie oft Sie Aussagen machen, bis Sie endgültig scheitern.

private static final ConditionFactory WAIT = await()
        .atMost(Duration.ofSeconds(6))
        .pollInterval(Duration.ofSeconds(1))
        .pollDelay(Duration.ofSeconds(1));

@Test
public void waitAndPoll(){
    triggerAsyncEvent();
    WAIT.untilAsserted(() -> {
        assertThat(findInDatabase(1).getState()).isEqualTo(State.SUCCESS);
    });
}

Auf diese Weise können Sie vermeiden, dass in Ihren Tests das instabile "Thread.sleep ()" verwendet wird.

Es ist jedoch viel einfacher, den Synchronisierungscode zu testen. Aus diesem Grund sollten Sie [die asynchrone Ausführung von der tatsächlichen Logik trennen](# die asynchrone Ausführung von der tatsächlichen Logik trennen).

Kein Bootstrap DI erforderlich (Feder)

(Frühling) Der DI-Framework-Bootstrap benötigt einige Sekunden, um mit dem Testen zu beginnen. Dies verlangsamt den Rückkopplungszyklus, insbesondere während der anfänglichen Entwicklung des Tests.

Deshalb verwende ich DI normalerweise nicht für Integrationstests. Ich instanziiere die erforderlichen Objekte manuell, indem ich "new" aufrufe und sie zusammenstelle. Wenn Sie die Konstruktorinjektion verwenden, ist dies ziemlich einfach. Meistens möchten Sie die von Ihnen geschriebene Geschäftslogik testen. Dafür wird kein DI benötigt. Schauen Sie sich als Beispiel Meine Beiträge zum Integrationstest an (https://phauer.com/2019/focus-integration-tests-mock-based-tests/#integration-tests).

Auf der anderen Seite wird Spring Boot 2.2 Funktionen einführen, die die Verwendung der Lazy Bean-Initialisierung vereinfachen und die DI-basierten Tests erheblich beschleunigen sollten. [^ Frühling] [^ Frühling]: Zum Zeitpunkt der Übersetzung veröffentlicht. Wahrscheinlich dies.

Machen Sie die Implementierung testbar

Verwenden Sie keinen statischen Zugriff. noch nie. Von jetzt an.

Statischer Zugriff ist ein Anti-Pattern. Erstens werden Abhängigkeiten und Nebenwirkungen verdeckt, der gesamte Code wird verwirrend und fehleranfällig. Zweitens beeinträchtigt der statische Zugriff die Testbarkeit. Sie können keine Objekte mehr austauschen. Beim Testen möchten Sie jedoch ein Modell oder ein reales Objekt mit unterschiedlichen Einstellungen verwenden (z. B. ein DAO, das auf eine Testdatenbank verweist).

Anstatt statisch auf Code zuzugreifen, schreiben Sie diesen Code in einer nicht statischen Methode, instanziieren Sie die Klasse und übergeben Sie dieses Objekt an den Konstruktor des erforderlichen Objekts.

//schlechtes Beispiel
public class ProductController {
    public List<ProductDTO> getProducts() {
        List<ProductEntity> products = ProductDAO.getProducts();
        return mapToDTOs(products);
    }
}
//Gutes Beispiel
public class ProductController {
    private ProductDAO dao;
    public ProductController(ProductDAO dao) {
        this.dao = dao;
    }
    public List<ProductDTO> getProducts() {
        List<ProductEntity> products = dao.getProducts();
        return mapToDTOs(products);
    }
}

Glücklicherweise bieten DI-Frameworks wie Spring eine einfache Möglichkeit, statischen Zugriff zu vermeiden. Weil es die Erstellung und Platzierung aller Objekte für uns übernimmt.

Parametrierung

Lassen Sie uns alle relevanten Teile der Klasse testen. Dies kann erreicht werden, indem Konstruktorparameter unter diesem Aspekt erstellt werden.

Angenommen, Ihr DAO verfügt über eine maximale Anzahl von Abfragen von 1000. Um dieses Limit zu testen, müssten Sie in Ihrem Test 1001 Datenbankeinträge generieren. Indem diese Obergrenze zu einem Konstruktorparameter gemacht wird, kann die Obergrenze festgelegt werden. In einer Produktionsumgebung ist dieser Parameter 1000. Im Test können Sie 2 einstellen. Zum Testen der Kappenfunktion sind nur drei Testeinträge erforderlich.

Konstruktorinjektion verwenden

Feldinjektion ist böse, weil sie weniger testbar ist. Sie müssen den Bootstrap der DI-Umgebung im Test oder die Hacky-Reflection-Magie verwenden. Deshalb ist die Konstruktorinjektion die bevorzugte Methode. Dies liegt daran, dass es einfacher ist, die abhängigen Objekte in Ihren Tests zu steuern.

Java benötigt ein paar Boilerplate.

//Gutes Beispiel
public class ProductController {

    private ProductDAO dao;
    private TaxClient client;

    public CustomerResource(ProductDAO dao, TaxClient client) {
        this.dao = dao;
        this.client = client;
    }
}

Kotlin macht das Gleiche prägnanter.

//Gutes Beispiel
class ProductController(
    private val dao: ProductDAO,
    private val client: TaxClient
){
}

Verwenden Sie nicht "Instant.now ()" oder "new Date ()"

Holen Sie sich nicht den aktuellen Zeitstempel, indem Sie "Instant.now ()" oder "new Date ()" in Ihrem Produktionscode aufrufen. Wenn Sie dieses Verhalten testen möchten.

//schlechtes Beispiel
public class ProductDAO {
    public void updateDateModified(String productId) {
        Instant now = Instant.now(); // !
        Update update = Update()
            .set("dateModified", now);
        Query query = Query()
            .addCriteria(where("_id").eq(productId));
        return mongoTemplate.updateOne(query, update, ProductEntity.class);
    }
}

Das Problem ist, dass der generierte Zeitstempel nicht durch den Test gesteuert werden kann. Sie können den genauen Wert nicht angeben, da er bei jedem Testlauf immer unterschiedlich ist. Verwenden Sie stattdessen die Java-Klasse "Clock".

//Gutes Beispiel
public class ProductDAO {
    private Clock clock; 

    public ProductDAO(Clock clock) {
        this.clock = clock;
    }

    public void updateProductState(String productId, State state) {
        Instant now = clock.instant();
        // ...
    }
}

Während des Tests können Sie jetzt ein Uhr-Mock generieren und an ProductDAO übergeben, um dieses Clock-Mock so zu konfigurieren, dass ein fester Zeitstempel zurückgegeben wird. Nach dem Aufruf von "updateProductState ()" wird bestätigt, ob der definierte Zeitstempel in die Datenbank eingefügt wurde.

Trennen Sie die asynchrone Ausführung von der tatsächlichen Logik

Das Testen von asynchronem Code ist schwierig. Bibliotheken wie Awaitility können helfen, aber es ist immer noch langweilig und das Testen ist immer noch instabil. Wenn möglich, ist es sinnvoll, die (häufig synchrone) Geschäftslogik von der asynchronen Ausführung zu trennen.

Sie können beispielsweise Ihre Geschäftslogik in einen ProductController einfügen und mit einem einfachen Synchronlauf testen. Asynchrone und parallele Logik werden in "ProductScheduler" zusammengefasst und können separat getestet werden.

//Gutes Beispiel
public class ProductScheduler {

    private ProductController controller;

    @Scheduled
    public void start() {
        CompletableFuture<String> usFuture = CompletableFuture.supplyAsync(() -> controller.doBusinessLogic(Locale.US));
        CompletableFuture<String> germanyFuture = CompletableFuture.supplyAsync(() -> controller.doBusinessLogic(Locale.GERMANY));
        String usResult = usFuture.get();
        String germanyResult = germanyFuture.get();
    }
}

Kotlin Mein Beitrag über Best Practices für Unit-Tests in Kotlin ist sehr Kotlin-spezifisch für das Schreiben von Tests in Kotlin. Enthält Empfehlungen.

Recommended Posts

Moderne Best Practices für Java-Tests
Moderne Best Practices für Java-Tests
Aktualisieren der Java-Umgebung von Windows mit Chocolatey
Für JAVA-Lernen (2018-03-16-01)
2017 IDE für Java
Java für Anweisung
Vergleichen Sie die PDF-Ausgabe in Java für Snapshot-Tests
[Java] Paket für die Verwaltung
[Java] für Anweisung / erweitert für Anweisung
Gegenmaßnahmen für OutOfMemoryError in Java
Probieren Sie Easy Ramdom, ein PropertyBase-Testtool für Java
NLP für Java (NLP4J) (2)
NLP für Java (NLP4J) (1)
Java-Debug-Ausführung [für Java-Anfänger]
Bücher zum Erlernen von Java
Java-Leistung Kapitel 2 Ansatz für Leistungstests
2018 Java Proficiency Test für Newcomer-Basics-
Optimieren Sie Java-Tests mit Spock
Java-Thread sicher für Sie
Java für Anfänger, Daten verstecken
[Java] Tipps zum Schreiben der Quelle
Java-Installationsort für Mac
Java-Anwendung für Anfänger: Stream
Java während und für Anweisungen
Was wählen Java-Ingenieure für das "automatische Testen" ihrer Browser? Selen? Selenid? Geb?