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
Indem Sie Hilfsfunktionen, parametrisierte Tests und leistungsstarke AssertJ-Assertions intensiv nutzen, Variablen nicht überbeanspruchen, nur die Relevanz überprüfen und das Schreiben von Tests für seltene Fälle vermeiden. ** Schreiben Sie einen kleinen, klaren Test. ** ** **
Schreiben Sie einen in sich geschlossenen Test, indem Sie alle relevanten Parameter klären, die Daten korrekt einfügen und die Zusammensetzung anstelle der Vererbung verwenden. ** ** **
Schreiben Sie einen ** Dump-Test [^ dumptest], indem Sie die Wiederverwendung von Produktionscode vermeiden und sich darauf konzentrieren, Ausgabewerte mit fest codierten Werten zu vergleichen. ** ** ** [^ dumptest]: Ich war mir nicht sicher, was der Test war, aber es scheint ein Test zu sein, der die erwarteten Werte fest codiert.
KISS-Prinzipien> DRY-Prinzipien
Konzentrieren Sie sich darauf, [^ Folie] vollständige vertikale Folien zu testen, die Verwendung von In-Memory-Datenbanken zu vermeiden und ** Tests zu schreiben, die der Produktion nahe kommen. ** ** ** [^ Folie]: Es scheint ein Integrationstest zu sein.
JUnit 5 und Assert J sind sehr gute Entscheidungen.
Vermeiden Sie statischen Zugriff, verwenden Sie die Konstruktorinjektion, verwenden Sie "Clock" [^ clock] und trennen Sie die Geschäftslogik von der asynchronen Ausführung. Arbeiten Sie hart daran, Ihre Implementierung einfach zu testen. [^ clock]: Die Klasse java.time.Clock.
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");
}
//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
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 #).
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");
}
createProductWithCategory ()
) und für komplexe Zusicherungen. Übergeben Sie nur Parameter, die für Ihren Test relevant sind, an Hilfsfunktionen. Verwenden Sie für andere Werte die entsprechenden Standardwerte. In Kotlin ist dies mit den Standardargumenten einfach zu bewerkstelligen. In Java müssen Sie Methodenketten und Überladungen verwenden, um Pseudo-Standardargumente zu erzielenìnsertIntoDatabase ()
)//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')}")
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.
//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.
//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.
Ü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:
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);
String responseJson = requestProductsByCategory("Office");
assertThat(toDTOs(responseJson))
.extracting(ProductDTO::getId)
.containsOnly("1", "2");
assertThat(actualProduct.getPrice()).isEqualTo(100);
//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"));
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.
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 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));
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.
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.
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.
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.
* 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 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
-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.
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 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.
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).
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)
)
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() {}
}
}
@ DisplayName
oder Kotlin BackquoteJava 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() {}
}
@ DisplayName
von JUnit5 *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`() {}
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");
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).
(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.
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.
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.
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
){
}
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.
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