Blog
Eigenschaftsbasierte Tests in Java mit JUnit-Quickcheck - Teil 2: Generatoren

In Teil 1 dieses Tutorials haben wir einen eigenschaftsbasierten Test (PBT) aus einem normalen JUnit-Test mit Basistypen erstellt. Lassen Sie uns nun das Domänenobjekt PostalParcel um eine Liste von Produkten erweitern.
[code language="java"] public class Product { private UUID uuid; private String name; private int Gewicht; public Product(UUID uuid, String name, int weight) { this.uuid = uuid; this.name = name; wenn(Gewicht > 0 ) { this.weight = weight; } sonst { throw new IllegalArgumentException("Gewicht nicht akzeptabel, kleiner als 0."); } } public int getWeight() { Gewicht zurückgeben; } } [/code] [code language="java"] public class PostalParcel { public static final double MAX_DELIVERY_COSTS = 4.99; public static final double MIN_DELIVERY_COSTS = 1.99; private UUID uuid; private Liste<Produkt> Produkte; public PostalParcel(UUID uuid, Liste<Produkt> Produkte) { this.uuid = uuid; this.products = products; } public double deliveryCosts() { wenn (Gewicht() > 20) { return MAX_DELIVERY_COSTS; } return MIN_DELIVERY_COSTS; } public int gewicht() { return products.stream().map(Product::getWeight).mapToInt(Integer::intValue).sum(); } } [/code] Alle Beispiele sind mit Java 8 geschrieben und können von meinem gitlab Repository heruntergeladen werden. Schreiben Sie einen Unit-Test für die Funktion deliveryCosts mit einer Fixture für die Entitäten. Eine Implementierung könnte etwa so aussehen: [code language="java"] private PostalParcel generatePostalParcel(int weight) { return new PostalParcel("UUID", generateProducts(weight)); } private Liste<Produkt> generateProducts(int weight) { Liste<Produkt> products = new ArrayList<>(); for (int i = 0; i < 5; i++) { products.add(new Product("UUID", "Name", weight/4)); } return products; } [/code] Wie im vorherigen Teil erläutert, decken diese Tests nicht das gesamte Verhalten ab, das wir hier testen. Wir werden dies mit Hilfe von Generatoren zu einem eigenschaftsbasierten Test umgestalten.
Generatoren
In diesem bestimmten Fall verwenden wir Entitäten als Eingabe für unsere Unit-Tests. Eine Möglichkeit, generierte Eigenschaften für Entitäten zu verwenden, besteht darin, für jedes der benötigten Argumente für unsere Entitäten einen Basistyp zu generieren. Es ist einfacher, die Entitäten von JUnit-Quickcheck generieren zu lassen, indem Sie Ihre eigenen Generatoren erstellen. Auf diese Weise wird Ihr Test nicht mit Eigenschaften aufgebläht und ist daher besser lesbar. Um einen Generator zu erstellen, müssen Sie drei Dinge tun. Erstellen Sie zunächst eine Klasse, die sich von generator< T > erstreckt: [code language="java"] public class PostalParcelGenerator extends Generator<PostalParcel> [/code] Zweitens implementieren Sie den Konstruktor mit einem Super für den Typ, den Sie erzeugen. Und schließlich überschreiben Sie die Funktion T generate: [code language="java"] public PostalParcelGenerator() { super(PostalParcel.class); } public PostalParcel generate(SourceOfRandomness sourceOfRandomness, GenerationStatus generationStatus) { return null; } [/code] Wir können den Generator in unserem eigenschaftsbasierten Test auf zwei Arten verwenden. Wir können die @From(T.class) Annotation vor Ihrer Entität verwenden. Diese Annotation teilt Junit-Quickcheck mit, welcher Generator verwendet werden soll, zum Beispiel: [code language="java"] public void deliveryCostsShouldBeMaxWhenWeightIsGreaterThan20(@From(PostalParcelGenerator.class) PostalParcel postalParcel) [/code] Es ist viel einfacher, den Serviceloader von JUnit-Quickcheck die Generatoren entdecken zu lassen, indem Sie eine Datei namens com.pholser.junit.quickcheck.generator.Generator in META-INF/services erstellen. In dieser Datei fügen Sie den Paketnamen und den Klassennamen des Generators hinzu. Auf diese Weise brauchen Sie in Ihren Unit-Tests noch weniger zu programmieren.
Implementieren erzeugen
Die Funktion generate sieht ziemlich einfach aus. Sie verlangt, dass Sie den Typ zurückgeben, den Sie erzeugen.
[code language="java"]
public PostalParcel generate(SourceOfRandomness sourceOfRandomness, GenerationStatus generationStatus) {
return new PostalParcel(gen().make(UUIDGenerator.class).generate(sourceOfRandomness, generationStatus),
generateProducts(sourceOfRandomness, generationStatus));
}
private List<Produc> generateProducts(SourceOfRandomness sourceOfRandomness, GenerationStatus generationStatus) {
ProductGenerator productGenerator = gen().make(ProductGenerator.class);
List<Product> products = new ArrayList<>();
int randomTotalWeight = sourceOfRandomness.nextInt(minWeight, maxWeight);
while(randomTotalWeight > 0) {
int maxWeight = sourceOfRandomness.nextInt(1, randomTotalWeight);
productGenerator.configureMaxWeight(maxWeight);
products.add(productGenerator.generate(sourceOfRandomness, generationStatus));
randomTotalWeight = randomTotalWeight-maxWeight;
}
return products;
}
[/code]
Hier gibt es mehrere JUnit-Quickcheck-Funktionen. Das erste ist das Argument SourceOfRandomness. Sie benötigen es, um zufällige primitive Typen zu erzeugen, wie Sie es bei der Generierung von randomTotalWeight sehen. Zweitens haben Sie das Argument GenerationStatus, das wir in diesem Beispiel nicht verwenden, aber wir können es nutzen, um die Ergebnisse einer Generierung für einen bestimmten Eigenschaftsparameter zu beeinflussen.
- Die Methode gen().make() macht die Generatoren für die aktuelle Instanz verfügbar und konfiguriert sie mit den Konfigurationsanmerkungen der Generatorklasse.
- Die Methode gen().type() fragt nach einem beliebigen Generator, der Instanzen des angegebenen Typs erzeugen kann. Wenn Sie dies für eine Zeichenkette verwenden, wird eine völlig zufällige Zeichenkette mit allen verfügbaren Zeichen (sogar chinesischen und koreanischen Zeichen) erzeugt.
Beachten Sie, dass, wie im vorigen Teil gesagt, wenn Sie eine Zeichenkette als Argument nehmen, die Werke von Shakespeare auf Japanisch und Koreanisch EINE gültige Eingabe sind. Wenn Sie können, verpacken Sie die Zeichenkette in einen eigenen Typ, wie in der Übung Object Calisthenics erklärt.
Generatoren konfigurieren
In der Implementierung unseres PostalParcelGenerators haben Sie vielleicht die Argumente minWeight und maxWeight bei der Erzeugung des randomTotalWeight bemerkt. Dies sind zwei Variablen, mit denen wir den Generator konfigurieren müssen. Wir können dies tun, indem wir eine öffentliche Funktion void namens configure erstellen. JUnit-Quickcheck verlangt von uns, dass wir diese Funktion so benennen, damit sie mit der Anmerkung übereinstimmt, die wir verwenden werden. In diesem Fall verwenden wir die standardmäßige JUnit-Quickcheck-Annotation @InRange, aber Sie können auch Ihre eigene Annotation verwenden. [code language="java"] private int minWeight = 1; private int maxWeight = Integer.MAX_VALUE; public void configure(InRange range) { this.minWeight = range.minInt() == Integer.MIN_VALUE ? 1 : range.minInt(); this.maxWeight = range.maxInt(); } [/code] Sie können Ihre Generatoren einfach mit dem Verhalten konfigurieren, das Sie für Ihre Testfälle benötigen. Das einzige Problem, das bleibt, ist, dass dort, wo Code geschrieben wird, Fehler gemacht werden können; wir alle machen Fehler. Das ist auch der Grund, warum ich in meinem vorherigen Beitrag dazu geraten habe, sowohl die Konfiguration per Annotation als auch assumeThat zu verwenden; mit diesem Ansatz lassen sich Fehler während Ihrer Tests leicht erkennen. Nachdem wir den Rest der Generatoren implementiert haben (die Sie in meinem Gitlab-Repository sehen können), können wir unsere PBT schreiben. [code language="java"] @RunWith(JUnitQuickcheck.class) public class PostalParcelPBTTest { @Property public void deliveryCostsShouldBeMaxWhenWeightIsGreaterThan20(@InRange(minInt = 21) PostalParcel postalParcel){ assumeThat(postalParcel.Gewicht(), greaterThan(20)); assertThat(postalParcel.deliveryCosts(), equalTo(com.baasie.pbt.part1.PostalParcel.MAX_DELIVERY_COSTS)); } @Property(trials = 25) public void deliveryCostsShouldBeMinWhenWeightIsLessThanOrEqualTo20(@InRange(maxInt = 20) PostalParcel postalParcel){ assumeThat(postalParcel.weight(), is(both(greaterThan(0)).and(lessThanOrEqualTo(20)))); assertThat(postalParcel.deliveryCosts(), equalTo(com.baasie.pbt.part1.PostalParcel.MIN_DELIVERY_COSTS)); [/code] } } Die Erstellung der Generatoren wird einige Anfangsinvestitionen kosten, aber danach profitieren Sie wirklich davon. Für jede PBT können Sie einfach den Typ des Generators und vielleicht einige zusätzliche Verhaltensweisen hinzufügen, die Sie damit erreichen möchten. Die geringe Investition wird sich langfristig in einer Zeitersparnis und einer besser entwickelten und geschriebenen Software niederschlagen. Außerdem finden die Tests Randfälle, die zu Fehlern in Ihrer Software führen könnten, und beheben diese, bevor sie auftreten.
Leistung
Nach meinem letzten Beitrag erhielt ich einen Kommentar mit einer Frage zur Leistung. Ich weiß nicht, wie die genaue Implementierung funktioniert, aber ich weiß, dass die Ausführung nicht 100x langsamer ist. Ich habe einen Lauf in meinem Intellij für beide Tests durchgeführt und der PBT ist etwa 10x langsamer. Meiner Meinung nach ist 10x nicht unangemessen, wenn man den Nutzen bedenkt, den Sie dadurch haben.
Damit ist Teil 2 dieses Tutorials abgeschlossen. Im nächsten Teil werde ich Ihnen zeigen, wie Sie fehlgeschlagene Tests wiederholen können.
Verfasst von

Kenny Baas-Schwegler
A lot of knowledge is lost when designing and building software — lost because of hand-overs in a telephone game, confusing communication by not having a shared language, discussing complexity without visualisation and by not leveraging the full potential and wisdom of the diversity of the people. That lost knowledge while creating software impacts the sustainability, quality and value of the software product. Kenny Baas-Schwegler is a strategic software delivery consultant and software architect with a focus on socio-technical systems. He blends IT approaches like Domain-Driven Design and Continuous Delivery and facilitates change with Deep Democracy by using visual and collaborative modelling practices like Eventstorming, Wardley mapping, context mapping and many more. Kenny empowers and collaboratively enables organisations, teams and groups of people in designing, architecting and building sustainable quality software products. One of Kenny's core principles is sharing knowledge. He does that by writing a blog on his website baasie.com and helping curate the Leanpub book visual collaboration tool. Besides writing, he also shares experience in the Domain-Driven Design community as an organiser of Virtual Domain-Driven Design (virtualddd.com) and Domain Driven Design Nederland. He enjoys being a public speaker by giving talks and hands-on workshops at conferences and meetups.
Unsere Ideen
Weitere Blogs
Contact



