Blog
Eigenschaftsbasierte Tests in Java mit JUnit-Quickcheck - Teil 1: Die Grundlagen

Um Ihnen zeigen zu können, was Property-based Testing (PBT) ist, müssen wir zunächst das Konzept einer Eigenschaft in Programmiersprachen verstehen. Da es sich hier um ein Java-Tutorial handelt, beginne ich mit Oracle und seiner Definition einer Eigenschaft in seinem Glossar:
Merkmale eines Objekts, die der Benutzer einstellen kann, z. B. die Farbe eines Fensters.
Eine Eigenschaft ist weder eine Variable/ein Feld noch eine Methode; sie ist etwas dazwischen, das in Ihrem Kontext immer wahr ist. Ein Beispiel ist das Gewicht eines Postpakets: Es ist immer größer als Null. In Java würde die folgende Beispielimplementierung folgen:
[code language="java"] public class PostalParcel { private int weight; private String uuid; public PostalParcel(String uuid, int weight) { this.uuid = uuid; if(weight > 0) { this.weight = weight; } else { throw new IllegalArgumentException(""); [/code] } } } In diesem Fall haben wir weight zu einer Eigenschaft gemacht, die immer größer als 0 sein muss. Wenn Sie Ihre Software entwerfen, ist es wichtig, nach diesen Eigenschaften zu suchen, da sie in der Regel mit einer Art von Geschäftsverhalten verbunden sind.
Was sind eigenschaftsbasierte Tests (PBT)?
Zurück zu PBT, lassen Sie uns eine Funktion zu PostalParcel hinzufügen, die die Versandkosten des Pakets bestimmt. Wenn das Gewicht größer als 20 ist, betragen die Versandkosten 4,99 Euro, ansonsten 1,99 Euro. Dazu schreiben wir zunächst unsere Tests, die ohne PBT in der Regel wie folgt aussehen werden: [code language="java"] public class PostalParcelTest { @Test public void deliveryCostsShouldBeMaxWhenWeightIsLargerThan20(){ PostalParcel postalParcel = new PostalParcel("uuid", 23); assertThat(postalParcel.deliveryCosts(), equalTo(PostalParcel.MAX_DELIVERY_COSTS)); } @Test public void deliveryCostsShouldBeMinWhenWeightIsLessThanOrEqualTo20(){ PostalParcel postalParcel = new PostalParcel("uuid", 19); assertThat(postalParcel.deliveryCosts(), equalTo(PostalParcel.MIN_DELIVERY_COSTS))); } @Test(expected = IllegalArgumentException) public void shouldThrowIllegalArgumentExceptionWhenWeightIsBelowOne() { PostalParcel postalParcel = new PostalParcel("uuid", -100); } [/code] Zweitens müssen wir diese Tests zum Erfolg führen, indem wir die Implementierung von deliveryCosts in ein Postal Parcel schreiben: [code language="java"] public static final double MAX_DELIVERY_COSTS = 4.99; public static final double MIN_DELIVERY_COSTS = 1.99; public double deliveryCosts() { if(weight > 20) { return MAX_DELIVERY_COSTS; } return MIN_DELIVERY_COSTS; } [/code] Wenn Sie die Tests ausführen, werden sie erfolgreich sein, aber wenn wir darüber nachdenken, bleiben 2 Probleme. Erstens haben wir unser Verhalten im Namen der Testfunktion sehr schön beschrieben, aber die Implementierung entspricht nicht dem Verhalten. Der zweite Test, deliveryCostsShouldBeMinWhenWeightIsLessThanOrEqualTo20, testet nur mit einem Gewicht von 19, aber das Verhalten sagt eindeutig Less Than Or Equal To 20. Wir können einen weiteren Test hinzufügen, der mit einem Gewicht von 20 testet, aber was ist mit den anderen Fällen? Es wäre eine Menge Arbeit und Zeitverschwendung, alle Tests selbst zu erstellen. Glücklicherweise kommt PBT zur Rettung! Außerdem, und darum geht es bei PBT, benötigen wir für den Test eine UUID als String. Da wir uns nicht um den Wert dieser Eigenschaft kümmern, verwenden wir in der Regel eine Fixture für einen Fall wie diesen. Es kann vorkommen, dass eine UUID bei der Ausführung des zu testenden Verhaltens jetzt oder in der Zukunft zu seltsamen Fehlern führen kann. Denn wir können alles als String-Eingabe verwenden, oder wie Romeu Moura einmal getwittert hat:
@kenny_baas wie ich in meinem Vortrag sagte: wenn Sie einen String als Argument nehmen, dann sind die Werke von Shakespeare auf Japanisch und Koreanisch EINE gültige Eingabe
- Romeu Moura (@malk_zameth) 23 februari 2017
Ich empfehle jedem, sich seinen Vortrag über Property-based Testing anzusehen.
Normalerweise machen Sie ein Value Object aus einer UUID, aber da ich Ihnen ein PBT-Beispiel zeigen möchte, belassen wir es vorerst bei einem String. Trotzdem können Sie bereits den Vorteil von PBT erkennen: Sie werden zum Nachdenken über Ihre Eingabewerte angeregt.
JUnit-Quickcheck Implementierung
Beginnen Sie mit dem Hinzufügen der JUnit-Quickcheck-Abhängigkeit zu unserem Projekt. Um JUnit-Quickcheck zu verwenden, müssen wir 2 Dinge ändern. Fügen Sie zunächst die @RunWith-Annotation über unserer Testklasse hinzu. [code language="java"] @RunWith(JUnitQuickcheck.class) [/code] Zweitens, statt @Test verwenden wir @Property. [code language="java"] @Property public void deliveryCostsShouldBeMaxWhenWeightIsLargerThan20(){ ..... [/code] JUnitQuickcheck wird nun jeden Eigenschaftstest 100 Mal ausführen. Den zusätzlichen Nutzen erhalten wir, wenn wir die Eigenschaftsparameter von JUnitQuickcheck generieren lassen: Für jeden Durchlauf wird ein zufälliger Parameter generiert. Lassen Sie uns diese hinzufügen: [code language="java"] @Property public void ZustellKostenSolltenMaxWennGewichtGrößerAls20(String uuid, int Gewicht) PostalParcel postalParcel = new PostalParcel(uuid, Gewicht); assertThat(postalParcel.deliveryCosts(), equalTo(PostalParcel.MAX_DELIVERY_COSTS))); } .... [/code] Wenn Sie dies ausführen, werden die Tests höchstwahrscheinlich fehlschlagen, denn die Zahl kann jetzt ein beliebiger int sein, aber was wir wollen, ist zum Beispiel größer als 20. Dies kann auf 2 Arten erreicht werden, entweder mit assumeThat von JUnit oder mit @InRange von JUnitQuickcheck vor der Variablen. [code language="java"] @RunWith(JUnitQuickcheck.class) public class PostalParcelTest { @Eigenschaft public void deliveryCostsShouldBeMaxWhenWeightIsGreaterThan20(String uuid, @InRange(minInt = 21) int weight){ assumeThat(weight, greaterThan(20)); PostalParcel postalParcel = new PostalParcel(uuid, weight); assertThat(postalParcel.deliveryCosts(), equalTo(PostalParcel.MAX_DELIVERY_COSTS)); } @Eigenschaft public void deliveryCostsShouldBeMinWhenWeightIsLessThanOrEqualTo20(String uuid, @InRange(minInt = 1, maxInt = 20) int weight){ assumeThat(weight, is(both(greaterThan(0)).and(lessThanOrEqualTo(20)))); PostalParcel postalParcel = new PostalParcel(uuid, weight); assertThat(postalParcel.deliveryCosts(), equalTo(PostalParcel.MIN_DELIVERY_COSTS)); } @Eigenschaft public void shouldThrowIllegalArgumentExceptionWhenWeightIsBelowOne(String uuid, @InRange(maxInt = 0) int weight) { assumeThat(weight, lessThanOrEqualTo(0)); IllegalArgumentException illegalArgumentException = null; versuchen { PostalParcel postalParcel = new PostalParcel(uuid, weight); } catch(IllegalArgumentException e) { illegalArgumentException = e; } assumeThat(illegalArgumentException, notNullValue()); } } [/code] Ich würde Ihnen raten, beide Optionen zu kombinieren, und zwar aus 2 Gründen. Erstens wird assumeThat bei kleinen Bereichen, wie im zweiten Beispiel, nicht ausreichend funktionieren. Sie werden Fehler erhalten, wenn die Annahmen verletzt werden. Durch das Hinzufügen von assumeThat werden Ihre Tests jedoch lesbarer und eindeutiger.
Versuche und Erfolg
JUnit-Quickcheck führt den Test standardmäßig 100 Mal aus, jeweils mit einem Zufallswert. In einigen Fällen, wie z.B. bei LessThanOrEqual20, kann die Ausführung von 100 eine Verschwendung sein (kann, weil es immer noch zufällig ist). Hierfür können wir den Trials-Modifikator auf die @Property(trials = 25) Annotation setzen. Dadurch wird JUnit-Quickcheck angewiesen, nur die hier festgelegte Anzahl von Versuchen durchzuführen, in diesem Fall sollten 25 ausreichen. Das sollte es auch, aber es ist immer noch zufällig, so dass es passieren kann, dass die Prüfungen beim ersten Mal erfolgreich sind, aber nach einer Weile fehlschlagen. Darin liegt der eigentliche Vorteil von PBT, denn es testet schließlich alle Eckfälle für Sie und findet mögliche Fehler in Ihrem System. Erfolg bedeutet nur, dass es dieses Mal keine Fehler gefunden hat, aber vielleicht beim nächsten Mal schon.
Jetzt haben wir unseren allerersten, grundlegenden, eigenschaftsbasierten Test für Java geschrieben. Das ist bereits sehr hilfreich. Im nächsten Teil dieses Tutorials werden wir tiefer in das Framework einsteigen und JUnit-Quickcheck unsere Entitäten als Eingabewerte für uns generieren lassen. Den Quellcode dieses Beispiels finden Sie auf meinem gitlab. Außerdem ist dieser Beitrag auf meinem privaten Blog veröffentlicht.
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.
Contact



