Οι αυτοματοποιημένες δοκιμές λογισμικού είναι κρίσιμης σημασίας για τη μακροπρόθεσμη ποιότητα, συντήρηση και επεκτασιμότητα έργων λογισμικού, και για την Java, το JUnit είναι η πορεία προς τον αυτοματισμό.
Ενώ το μεγαλύτερο μέρος αυτού του άρθρου θα επικεντρωθεί στη σύνταξη ισχυρών δοκιμών μονάδας και στη χρήση έγχυσης, χλευασμού και εξάρτησης, θα συζητήσουμε επίσης τις δοκιμές JUnit και ενσωμάτωσης.
ο Πλαίσιο δοκιμής JUnit είναι ένα κοινό, δωρεάν και ανοιχτού κώδικα εργαλείο για τη δοκιμή έργων που βασίζονται σε Java.
Από αυτό το γράψιμο, JUnit 4 είναι η τρέχουσα μεγάλη κυκλοφορία, η οποία κυκλοφόρησε πριν από περισσότερα από 10 χρόνια, με την τελευταία ενημέρωση να είναι πριν από περισσότερα από δύο χρόνια.
JUnit 5 (με τα μοντέλα προγραμματισμού και επέκτασης του Δία) βρίσκεται σε ενεργή ανάπτυξη. Υποστηρίζει καλύτερα τις λειτουργίες γλώσσας που εισήχθησαν στο Java 8 και περιλαμβάνει άλλα νέα, ενδιαφέροντα χαρακτηριστικά. Ορισμένες ομάδες μπορεί να βρουν το JUnit 5 έτοιμο για χρήση, ενώ άλλες μπορεί να συνεχίσουν να χρησιμοποιούν το JUnit 4 έως ότου κυκλοφορήσει επίσημα το 5. Θα δούμε παραδείγματα και από τα δύο.
Οι δοκιμές JUnit μπορούν να εκτελεστούν απευθείας στο IntelliJ, αλλά μπορούν επίσης να εκτελεστούν σε άλλα IDE όπως το Eclipse, το NetBeans ή ακόμα και τη γραμμή εντολών.
Οι δοκιμές πρέπει να εκτελούνται πάντα κατά το χρόνο κατασκευής, ειδικά οι δοκιμές μονάδας. Ένα build με τυχόν αποτυχημένες δοκιμές θα πρέπει να θεωρείται αποτυχημένο, ανεξάρτητα από το αν το πρόβλημα είναι στην παραγωγή ή στον κωδικό δοκιμής - αυτό απαιτεί πειθαρχία από την ομάδα και προθυμία να δώσει την υψηλότερη προτεραιότητα στην επίλυση αποτυχημένων δοκιμών, αλλά είναι απαραίτητο να τηρηθεί πνεύμα αυτοματισμού.
Οι δοκιμές JUnit μπορούν επίσης να εκτελεστούν και να αναφερθούν από συστήματα συνεχούς ολοκλήρωσης όπως το Jenkins. Τα έργα που χρησιμοποιούν εργαλεία όπως το Gradle, το Maven ή το Ant έχουν το πρόσθετο πλεονέκτημα ότι είναι σε θέση να εκτελέσουν δοκιμές ως μέρος της διαδικασίας κατασκευής.
Ως δείγμα έργου Gradle για το JUnit 5, δείτε το Ενότητα Gradle του οδηγού χρήστη JUnit και το junit5-samples.git αποθήκη. Σημειώστε ότι μπορεί επίσης να εκτελέσει δοκιμές που χρησιμοποιούν το JUnit 4 API (αναφέρεται ως 'σοδειά' ).
Το έργο μπορεί να δημιουργηθεί στο IntelliJ μέσω της επιλογής μενού Αρχείο> Άνοιγμα…> πλοηγηθείτε στο junit-gradle-consumer sub-directory
> ΟΚ> Άνοιγμα ως έργο> ΟΚ για εισαγωγή του έργου από το Gradle.
Για το Eclipse, το Πρόσθετο Buildship Gradle μπορεί να εγκατασταθεί από τη Βοήθεια> Eclipse Marketplace… Στη συνέχεια, το έργο μπορεί να εισαχθεί με το αρχείο> Εισαγωγή…> Gradle> Gradle Project> Next> Next> Browse to the junit-gradle-consumer
υποκατάλογος> Επόμενο> Επόμενο> Τέλος.
Μετά τη δημιουργία του έργου Gradle σε IntelliJ ή Eclipse, εκτελείτε το Gradle build
Η εργασία θα περιλαμβάνει την εκτέλεση όλων των δοκιμών JUnit με το test
έργο. Σημειώστε ότι οι δοκιμές ενδέχεται να παραλειφθούν σε επόμενες εκτελέσεις build
εάν δεν έγιναν αλλαγές στον κώδικα.
Για το JUnit 4, δείτε το JUnit's χρήση με το Gradle wiki .
Για το JUnit 5, ανατρέξτε στο Ενότητα Maven του οδηγού χρήστη και το junit5-samples.git αποθετήριο για ένα παράδειγμα έργου Maven. Αυτό μπορεί επίσης να εκτελέσει vintage δοκιμές (αυτές που χρησιμοποιούν το JUnit 4 API).
Στο IntelliJ, χρησιμοποιήστε το Αρχείο> Άνοιγμα…> πλοηγηθείτε στο junit-maven-consumer/pom.xml
> ΟΚ> Άνοιγμα ως έργο. Οι δοκιμές μπορούν στη συνέχεια να εκτελεστούν από το Maven Projects> junit5-maven-καταναλωτής> Lifecycle> Test.
Στο Eclipse, χρησιμοποιήστε το αρχείο> Εισαγωγή…> Maven> Υφιστάμενα έργα Maven> Επόμενο> Περιήγηση στο junit-maven-consumer
κατάλογος> Με το pom.xml
επιλεγμένο> Τέλος.
ποια είναι η αρχή του σχεδιασμού
Οι δοκιμές μπορούν να εκτελεστούν εκτελώντας το έργο καθώς το Maven build…> καθορίζει τον στόχο του test
> Εκτελέστε.
Για το JUnit 4, δείτε JUnit στο αποθετήριο Maven .
Εκτός από την εκτέλεση δοκιμών μέσω εργαλείων κατασκευής όπως το Gradle ή το Maven, πολλά IDE μπορούν να εκτελέσουν απευθείας δοκιμές JUnit.
IntelliJ IDEA Απαιτείται 2016.2 ή μεταγενέστερη έκδοση για δοκιμές JUnit 5, ενώ οι δοκιμές JUnit 4 πρέπει να λειτουργούν σε παλαιότερες εκδόσεις IntelliJ.
Για τους σκοπούς αυτού του άρθρου, μπορεί να θέλετε να δημιουργήσετε ένα νέο έργο στο IntelliJ από ένα από τα αποθετήρια του GitHub ( JUnit5IntelliJ.git ή JUnit4IntelliJ.git ), που περιλαμβάνουν όλα τα αρχεία στο απλό Person
παράδειγμα τάξης και χρησιμοποιήστε τις ενσωματωμένες βιβλιοθήκες JUnit. Η δοκιμή μπορεί να εκτελεστεί με το Run> Run 'All Tests'. Η δοκιμή μπορεί επίσης να εκτελεστεί στο IntelliJ από το PersonTest
τάξη.
Αυτά τα αποθετήρια δημιουργήθηκαν με νέα έργα IntelliJ Java και δημιουργήθηκαν οι δομές καταλόγου src/main/java/com/example
και src/test/java/com/example
. Το src/main/java
Ο κατάλογος καθορίστηκε ως φάκελος προέλευσης ενώ src/test/java
καθορίστηκε ως φάκελος πηγής δοκιμής. Μετά τη δημιουργία του PersonTest
κλάση με μέθοδο δοκιμής με σχολιασμό με @Test
, ενδέχεται να αποτύχει να μεταγλωττίσει, οπότε το IntelliJ προσφέρει την πρόταση να προσθέσετε JUnit 4 ή JUnit 5 στη διαδρομή κλάσης που μπορεί να φορτωθεί από τη διανομή IntelliJ IDEA (βλ. αυτές οι απαντήσεις στο Stack Overflow για περισσότερες λεπτομέρειες). Τέλος, μια διαμόρφωση εκτέλεσης JUnit προστέθηκε για όλες τις δοκιμές.
Δείτε επίσης το Οδηγίες οδηγιών δοκιμών IntelliJ .
Ενα άδειο Ιάβα Το έργο στο Eclipse δεν θα έχει δοκιμαστικό ριζικό κατάλογο. Αυτό προστέθηκε από το έργο Properties> Java Build Path> Add Folder…> Create New Folder…> καθορίστε το όνομα του φακέλου> Finish. Ο νέος κατάλογος θα επιλεγεί ως φάκελος προέλευσης. Κάντε κλικ στο OK και στους δύο υπόλοιπους διαλόγους.
Οι δοκιμές JUnit 4 μπορούν να δημιουργηθούν με το File> New> JUnit Test Case. Επιλέξτε 'New JUnit 4 test' και τον φάκελο προέλευσης που δημιουργήθηκε πρόσφατα για δοκιμές. Καθορίστε μια 'κλάση υπό δοκιμή' και ένα 'πακέτο', βεβαιωθείτε ότι το πακέτο ταιριάζει με την υπό δοκιμή κλάση. Στη συνέχεια, καθορίστε ένα όνομα για την τάξη δοκιμής. Αφού ολοκληρώσετε τον οδηγό, εάν σας ζητηθεί, επιλέξτε 'Προσθήκη βιβλιοθήκης JUnit 4' στη διαδρομή κατασκευής. Το έργο ή η ατομική δοκιμαστική τάξη μπορούν στη συνέχεια να εκτελεστούν ως JUnit Test. Δείτε επίσης Eclipse Writing και εκτέλεση δοκιμών JUnit .
Το NetBeans υποστηρίζει μόνο δοκιμές JUnit 4. Τα μαθήματα δοκιμής μπορούν να δημιουργηθούν σε ένα έργο NetBeans Java με Αρχείο> Νέο αρχείο…> Δοκιμές μονάδας> Δοκιμή JUnit ή Δοκιμή για υπάρχουσα τάξη. Από προεπιλογή, ο δοκιμαστικός ριζικός κατάλογος ονομάζεται test
στον κατάλογο έργων.
Ας ρίξουμε μια ματιά σε ένα απλό παράδειγμα κώδικα παραγωγής και τον αντίστοιχο κωδικό δοκιμής μονάδας για ένα πολύ απλό Person
τάξη. Μπορείτε να κατεβάσετε το δείγμα κώδικα από το my έργο github και ανοίξτε το μέσω IntelliJ.
package com.example; class Person { private final String givenName; private final String surname; Person(String givenName, String surname) { this.givenName = givenName; this.surname = surname; } String getDisplayName() { return surname + ', ' + givenName; } }
ο αμετάβλητος Person
η τάξη έχει έναν κατασκευαστή και ένα getDisplayName()
μέθοδος. Θέλουμε να δοκιμάσουμε ότι getDisplayName()
επιστρέφει το όνομα μορφοποιημένο όπως περιμένουμε. Εδώ είναι ο κωδικός δοκιμής για δοκιμή μίας μονάδας (JUnit 5):
package com.example; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class PersonTest { @Test void testGetDisplayName() { Person person = new Person('Josh', 'Hayden'); String displayName = person.getDisplayName(); assertEquals('Hayden, Josh', displayName); } }
PersonTest
χρησιμοποιεί το JUnit 5's @Test
και ισχυρισμός. Για το JUnit 4, το PersonTest
πρέπει να είναι δημόσια και να χρησιμοποιούνται διαφορετικές εισαγωγές Εδώ είναι το JUnit 4 παράδειγμα Gist .
Κατά την εκτέλεση του PersonTest
τάξη στο IntelliJ, οι δοκιμές περνούν και οι ενδείξεις UI είναι πράσινες.
Αν και δεν απαιτείται, χρησιμοποιούμε κοινές συμβάσεις για την ονομασία της κατηγορίας δοκιμής. Συγκεκριμένα, ξεκινάμε με το όνομα της τάξης που δοκιμάζεται (Person
) και προσθέτουμε 'Δοκιμή' σε αυτήν (PersonTest
). Η ονομασία της μεθόδου δοκιμής είναι παρόμοια, ξεκινώντας με τη μέθοδο που δοκιμάστηκε (getDisplayName()
) και προτείνοντας τη «δοκιμή» σε αυτήν (testGetDisplayName()
). Ενώ υπάρχουν πολλές άλλες απολύτως αποδεκτές συμβάσεις για την ονομασία μεθόδων δοκιμών, είναι σημαντικό να είστε συνεπείς σε όλη την ομάδα και το έργο.
Όνομα στην παραγωγή | Όνομα στη δοκιμή |
---|---|
Πρόσωπο | Δοκιμή προσώπου |
getDisplayName() | testDisplayName() |
Χρησιμοποιούμε επίσης τη σύμβαση δημιουργίας του κωδικού δοκιμής PersonTest
τάξη στο ίδιο πακέτο (com.example
) με τον κωδικό παραγωγής Person
τάξη. Εάν χρησιμοποιήσαμε ένα διαφορετικό πακέτο για δοκιμές, θα πρέπει να χρησιμοποιούμε το κοινό πρόσβαση επεξεργασίας σε κλάσεις κωδικών παραγωγής, κατασκευαστές και μεθόδους που αναφέρονται από δοκιμές μονάδας, ακόμα και όταν δεν είναι κατάλληλο, επομένως είναι καλύτερα να τα διατηρείτε στο ίδιο πακέτο. Ωστόσο, χρησιμοποιούμε ξεχωριστούς καταλόγους πηγών (src/main/java
και src/test/java
), καθώς γενικά δεν θέλουμε να συμπεριλάβουμε τον δοκιμαστικό κώδικα σε εκδόσεις παραγωγής που κυκλοφόρησαν.
Το @Test
σχολιασμός (JUnit 4 / 5 ) λέει στο JUnit να εκτελέσει το testGetDisplayName()
μέθοδος ως μέθοδος δοκιμής και αναφέρετε εάν περνά ή αποτυγχάνει. Όσο όλοι οι ισχυρισμοί (εάν υπάρχουν) περάσουν και δεν υπάρχουν εξαιρέσεις, ο έλεγχος θεωρείται επιτυχής.
Ο κωδικός δοκιμής μας ακολουθεί το μοτίβο δομής του Arrange-Act-Assert (AAA) . Άλλα κοινά μοτίβα περιλαμβάνουν το Given-When-Then και το Setup-Exercise-Verify-Teardown (το Teardown συνήθως δεν απαιτείται ρητά για δοκιμές μονάδας), αλλά χρησιμοποιούμε AAA σε αυτό το άρθρο.
Ας ρίξουμε μια ματιά στον τρόπο με τον οποίο το δοκιμαστικό μας παράδειγμα ακολουθεί το AAA. Η πρώτη γραμμή, η «τακτοποίηση» δημιουργεί ένα Person
αντικείμενο που θα δοκιμαστεί:
Person person = new Person('Josh', 'Hayden');
Η δεύτερη γραμμή, η «πράξη» γυμνάσια ο κωδικός παραγωγής Person.getDisplayName()
μέθοδος:
String displayName = person.getDisplayName();
Η τρίτη γραμμή, το 'assert', επιβεβαιώνει ότι το αποτέλεσμα είναι όπως αναμενόταν.
assertEquals('Hayden, Josh', displayName);
Εσωτερικά, το assertEquals()
Η κλήση χρησιμοποιεί τη μέθοδο ίση με το συμβολοσειρά 'Hayden, Josh' String για την επαλήθευση της πραγματικής αξίας που επιστρέφεται από τους αγώνες του κωδικού παραγωγής (displayName
). Εάν δεν ταιριάζει, η δοκιμή θα έχει χαρακτηριστεί ως αποτυχημένη.
Σημειώστε ότι οι δοκιμές έχουν συχνά περισσότερες από μία γραμμές για καθεμία από αυτές τις φάσεις AAA.
Τώρα που έχουμε καλύψει ορισμένες συμβάσεις δοκιμών, ας στρέψουμε την προσοχή μας στο να κάνουμε τον κώδικα παραγωγής δοκιμή.
Επιστρέφουμε στο Person
τάξη, όπου έχω εφαρμόσει μια μέθοδο για να επιστρέψω την ηλικία ενός ατόμου με βάση την ημερομηνία γέννησής του. Τα παραδείγματα κώδικα απαιτούν από την Java 8 να επωφεληθεί από νέα ημερομηνία και λειτουργικά API. Αυτό είναι το νέο Person.java
η τάξη μοιάζει με:
// ... class Person { // ... private final LocalDate dateOfBirth; Person(String givenName, String surname, LocalDate dateOfBirth) { // ... this.dateOfBirth = dateOfBirth; } // ... long getAge() { return ChronoUnit.YEARS.between(dateOfBirth, LocalDate.now()); } public static void main(String... args) { Person person = new Person('Joey', 'Doe', LocalDate.parse('2013-01-12')); System.out.println(person.getDisplayName() + ': ' + person.getAge() + ' years'); // Doe, Joey: 4 years } }
Η εκτέλεση αυτού του μαθήματος (τη στιγμή της γραφής) ανακοινώνει ότι ο Joey είναι 4 ετών. Ας προσθέσουμε μια μέθοδο δοκιμής:
// ... class PersonTest { // ... @Test void testGetAge() { Person person = new Person('Joey', 'Doe', LocalDate.parse('2013-01-12')); long age = person.getAge(); assertEquals(4, age); } }
Περνά σήμερα, αλλά τι γίνεται όταν το τρέχουμε ένα χρόνο από τώρα; Αυτή η δοκιμή δεν είναι ντετερμινιστική και εύθραυστη καθώς το αναμενόμενο αποτέλεσμα εξαρτάται από την τρέχουσα ημερομηνία του συστήματος που εκτελεί τη δοκιμή.
Όταν τρέχουμε στην παραγωγή θέλουμε να χρησιμοποιήσουμε την τρέχουσα ημερομηνία, LocalDate.now()
, για τον υπολογισμό της ηλικίας του ατόμου, αλλά για να κάνουμε μια ντετερμινιστική δοκιμή ακόμη και σε ένα χρόνο από τώρα, οι δοκιμές πρέπει να παρέχουν το δικό τους currentDate
αξίες.
Αυτό είναι γνωστό ως ένεση εξάρτησης. Δεν θέλουμε το Person
αντικείμενο για να προσδιορίσει την ίδια την τρέχουσα ημερομηνία, αλλά αντίθετα θέλουμε να μεταδώσουμε αυτήν τη λογική ως εξάρτηση. Οι δοκιμές μονάδας θα χρησιμοποιήσουν μια γνωστή, στεγανή τιμή και ο κωδικός παραγωγής θα επιτρέψει την παροχή της πραγματικής τιμής από το σύστημα κατά το χρόνο εκτέλεσης.
Ας προσθέσουμε ένα LocalDate
προμηθευτής σε Person.java
:
// ... class Person { // ... private final LocalDate dateOfBirth; private final Supplier currentDateSupplier; Person(String givenName, String surname, LocalDate dateOfBirth) { this(givenName, surname, dateOfBirth, LocalDate::now); } // Visible for testing Person(String givenName, String surname, LocalDate dateOfBirth, Supplier currentDateSupplier) { // ... this.dateOfBirth = dateOfBirth; this.currentDateSupplier = currentDateSupplier; } // ... long getAge() { return ChronoUnit.YEARS.between(dateOfBirth, currentDateSupplier.get()); } public static void main(String... args) { Person person = new Person('Joey', 'Doe', LocalDate.parse('2013-01-12')); System.out.println(person.getDisplayName() + ': ' + person.getAge() + ' years'); // Doe, Joey: 4 years } }
Για να γίνει πιο εύκολο να δοκιμάσετε το getAge()
μέθοδος, το αλλάξαμε για χρήση currentDateSupplier
, a LocalDate
προμηθευτή, για την ανάκτηση της τρέχουσας ημερομηνίας. Εάν δεν γνωρίζετε τι είναι ο προμηθευτής, σας προτείνω να διαβάσετε Ενσωματωμένες λειτουργικές διεπαφές Lambda .
αρχή του σχεδιασμού στην τέχνη
Προσθέσαμε επίσης μια ένεση εξάρτησης: Ο νέος κατασκευαστής δοκιμών επιτρέπει στις δοκιμές να παρέχουν τις δικές τους τρέχουσες τιμές ημερομηνίας. Ο αρχικός κατασκευαστής καλεί αυτόν τον νέο κατασκευαστή, περνώντας μια στατική μέθοδο αναφοράς της LocalDate::now
, η οποία παρέχει ένα LocalDate
αντικείμενο, οπότε η κύρια μέθοδος μας εξακολουθεί να λειτουργεί όπως και πριν. Τι γίνεται με τη μέθοδο δοκιμής μας; Ας ενημερώσουμε PersonTest.java
:
// ... class PersonTest { // ... @Test void testGetAge() { LocalDate dateOfBirth = LocalDate.parse('2013-01-02'); LocalDate currentDate = LocalDate.parse('2017-01-17'); Person person = new Person('Joey', 'Doe', dateOfBirth, ()->currentDate); long age = person.getAge(); assertEquals(4, age); } }
Το τεστ εγχέει τώρα το δικό του currentDate
τιμή, οπότε η δοκιμή μας θα εξακολουθήσει να ισχύει όταν εκτελείται το επόμενο έτος ή κατά τη διάρκεια οποιουδήποτε έτους. Αυτό συνήθως αναφέρεται ως γκρίνια , ή παρέχοντας μια γνωστή τιμή για επιστροφή, αλλά πρώτα πρέπει να αλλάξουμε Person
για να επιτρέψει την ένεση αυτής της εξάρτησης.
Σημειώστε το σύνταξη λάμδα (()->currentDate
) κατά την κατασκευή του Person
αντικείμενο. Αυτό αντιμετωπίζεται ως προμηθευτής ενός LocalDate
, όπως απαιτείται από τον νέο κατασκευαστή.
Είμαστε έτοιμοι για το Person
αντικείμενο - του οποίου όλη η ύπαρξη ήταν στη μνήμη JVM - να επικοινωνήσει με τον έξω κόσμο. Θέλουμε να προσθέσουμε δύο μεθόδους: το publishAge()
μέθοδος, η οποία θα δημοσιεύσει την τρέχουσα ηλικία του ατόμου και την getThoseInCommon()
μέθοδος, η οποία θα εμφανίσει ονόματα διάσημων ανθρώπων που μοιράζονται τα ίδια γενέθλια ή έχουν την ίδια ηλικία με την Person
. Ας υποθέσουμε ότι υπάρχει μια υπηρεσία RESTful με την οποία μπορούμε να αλληλεπιδράσουμε με την ονομασία 'People Birthdays'. Έχουμε έναν πελάτη Java για αυτό που αποτελείται από την ενιαία τάξη, BirthdaysClient
.
package com.example.birthdays; import java.io.IOException; import java.util.Arrays; import java.util.Collection; public class BirthdaysClient { public void publishRegularPersonAge(String name, long age) throws IOException { System.out.println('publishing ' + name + ''s age: ' + age); // HTTP POST with name and age and possibly throw an exception } public Collection findFamousNamesOfAge(long age) throws IOException { System.out.println('finding famous names of age ' + age); return Arrays.asList(/* HTTP GET with age and possibly throw an exception */); } public Collection findFamousNamesBornOn(int month, int dayOfMonth) throws IOException { System.out.println('finding famous names born on day ' + dayOfMonth + ' of month ' + month); return Arrays.asList(/* HTTP GET with month and day and possibly throw an exception */); } }
Ας βελτιώσουμε το Person
τάξη. Ξεκινάμε προσθέτοντας μια νέα μέθοδο δοκιμής για την επιθυμητή συμπεριφορά του publishAge()
. Γιατί να ξεκινήσετε με το τεστ και όχι τη λειτουργικότητα; Ακολουθούμε τις αρχές της δοκιμαστικής ανάπτυξης (επίσης γνωστής ως TDD), όπου γράφουμε πρώτα το τεστ και μετά τον κώδικα για να το πετύχουμε.
// … class PersonTest { // … @Test void testPublishAge() { LocalDate dateOfBirth = LocalDate.parse('2000-01-02'); LocalDate currentDate = LocalDate.parse('2017-01-01'); Person person = new Person('Joe', 'Sixteen', dateOfBirth, ()->currentDate); person.publishAge(); } }
Σε αυτό το σημείο, ο κωδικός δοκιμής αποτυγχάνει να συγκεντρωθεί επειδή δεν έχουμε δημιουργήσει το publishAge()
μέθοδος που καλεί. Μόλις δημιουργήσουμε ένα κενό Person.publishAge()
μέθοδος, όλα περνούν. Είμαστε τώρα έτοιμοι για τη δοκιμή για να επαληθεύσουμε ότι η ηλικία του ατόμου δημοσιεύεται πραγματικά στο BirthdaysClient
.
Δεδομένου ότι πρόκειται για μονάδα δοκιμής, θα πρέπει να τρέχει γρήγορα και στη μνήμη, οπότε η δοκιμή θα κατασκευάσει το Person
αντικείμενο με πλαστή BirthdaysClient
οπότε δεν υποβάλλει ένα αίτημα ιστού. Στη συνέχεια, το τεστ θα χρησιμοποιήσει αυτό το πλαστό αντικείμενο για να επιβεβαιώσει ότι κλήθηκε όπως αναμενόταν. Για να γίνει αυτό, θα προσθέσουμε μια εξάρτηση από το Πλαίσιο Mockito (Άδεια MIT) για τη δημιουργία πλαστών αντικειμένων και, στη συνέχεια, δημιουργήστε ένα πλαστό BirthdaysClient
αντικείμενο:
// ... import com.example.birthdays.BirthdaysClient; // ... import static org.mockito.Mockito.mock; class PersonTest { private BirthdaysClient birthdaysClient = mock(BirthdaysClient.class); // ... @Test void testPublishAge() { // ... Person person = new Person('Joe', 'Sixteen', dateOfBirth, ()->currentDate, birthdaysClient); // ... } }
Επίσης, αυξήσαμε την υπογραφή του Person
κατασκευαστής για να λάβει ένα BirthdaysClient
αντικείμενο και άλλαξε το τεστ για να εισαγάγει το πλαστό BirthdaysClient
αντικείμενο.
Στη συνέχεια, προσθέτουμε στο τέλος του testPublishAge
μια προσδοκία ότι η BirthdaysClient
λέγεται. Person.publishAge()
πρέπει να το ονομάσουμε, όπως φαίνεται στο νέο μας PersonTest.java
:
// ... class PersonTest { // ... @Test void testPublishAge() throws IOException { // ... Person person = new Person('Joe', 'Sixteen', dateOfBirth, ()->currentDate, birthdaysClient); verifyZeroInteractions(birthdaysClient); person.publishAge(); verify(birthdaysClient).publishRegularPersonAge('Joe Sixteen', 16); } }
Το Mockito μας έχει βελτιωθεί BirthdaysClient
παρακολουθεί όλες τις κλήσεις που έχουν πραγματοποιηθεί σύμφωνα με τις μεθόδους της, με αυτόν τον τρόπο επαληθεύουμε ότι δεν έχουν πραγματοποιηθεί κλήσεις προς BirthdaysClient
με το verifyZeroInteractions()
μέθοδο πριν από την κλήση publishAge()
. Αν και αναμφισβήτητα δεν είναι απαραίτητο, με αυτόν τον τρόπο διασφαλίζουμε ότι ο κατασκευαστής δεν πραγματοποιεί απατεώνες. Στο verify()
γραμμή, καθορίζουμε πώς περιμένουμε την κλήση στο BirthdaysClient
να κοιτάξω.
Λάβετε υπόψη ότι επειδή το publishRegularPersonAge έχει την υπογραφή του IOException, το προσθέτουμε και στην υπογραφή της μεθόδου δοκιμής.
Σε αυτό το σημείο, η δοκιμή αποτυγχάνει:
Wanted but not invoked: birthdaysClient.publishRegularPersonAge( 'Joe Sixteen', 16L ); -> at com.example.PersonTest.testPublishAge(PersonTest.java:40)
Αυτό αναμένεται, δεδομένου ότι δεν έχουμε εφαρμόσει ακόμη τις απαιτούμενες αλλαγές στο Person.java
, καθώς ακολουθούμε δοκιμαστική ανάπτυξη. Τώρα θα κάνουμε αυτό το τεστ επιτυχίας κάνοντας τις απαραίτητες αλλαγές:
// ... class Person { // ... private final BirthdaysClient birthdaysClient; Person(String givenName, String surname, LocalDate dateOfBirth) { this(givenName, surname, dateOfBirth, LocalDate::now, new BirthdaysClient()); } // Visible for testing Person(String givenName, String surname, LocalDate dateOfBirth, Supplier currentDateSupplier, BirthdaysClient birthdaysClient) { // ... this.birthdaysClient = birthdaysClient; } // ... void publishAge() { String nameToPublish = givenName + ' ' + surname; long age = getAge(); try { birthdaysClient.publishRegularPersonAge(nameToPublish, age); } catch (IOException e) { // TODO handle this! e.printStackTrace(); } } }
Φτιάξαμε τον κατασκευαστή κώδικα παραγωγής ένα νέο BirthdaysClient
, και publishAge()
τώρα καλεί το birthdaysClient
. Όλες οι εξετάσεις περνούν. όλα είναι πράσινα. Εξαιρετική! Αλλά προσέξτε ότι publishAge()
καταπίνει το IOException. Αντί να το αφήσουμε να φουσκώσει, θέλουμε να το τυλίξουμε με το δικό μας PersonException σε ένα νέο αρχείο που ονομάζεται PersonException.java
:
package com.example; public class PersonException extends Exception { public PersonException(String message, Throwable cause) { super(message, cause); } }
Εφαρμόζουμε αυτό το σενάριο ως μια νέα μέθοδο δοκιμής στο PersonTest.java
:
// ... class PersonTest { // ... @Test void testPublishAge_IOException() throws IOException { LocalDate dateOfBirth = LocalDate.parse('2000-01-02'); LocalDate currentDate = LocalDate.parse('2017-01-01'); Person person = new Person('Joe', 'Sixteen', dateOfBirth, ()->currentDate, birthdaysClient); IOException ioException = new IOException(); doThrow(ioException).when(birthdaysClient).publishRegularPersonAge('Joe Sixteen', 16); try { person.publishAge(); fail('expected exception not thrown'); } catch (PersonException e) { assertSame(ioException, e.getCause()); assertEquals('Failed to publish Joe Sixteen age 16', e.getMessage()); } } }
Το Mockito doThrow()
κλήση στέλεχος birthdaysClient
για την εξαίρεση όταν το publishRegularPersonAge()
καλείται μέθοδος. Εάν το PersonException
δεν πετάμε, αποτυγχάνουμε το τεστ. Διαφορετικά ισχυριζόμαστε ότι η εξαίρεση ήταν σωστά αλυσοδεμένος με το IOException και επαληθεύστε ότι το μήνυμα εξαίρεσης είναι όπως αναμένεται. Αυτήν τη στιγμή, επειδή δεν έχουμε εφαρμόσει κανένα χειρισμό στον κώδικα παραγωγής μας, η δοκιμή μας αποτυγχάνει επειδή η αναμενόμενη εξαίρεση δεν απορρίφθηκε. Να τι πρέπει να αλλάξουμε στο Person.java
για να κάνετε το τεστ επιτυχίας:
// ... class Person { // ... void publishAge() throws PersonException { // ... try { // ... } catch (IOException e) { throw new PersonException('Failed to publish ' + nameToPublish + ' age ' + age, e); } } }
Εφαρμόζουμε τώρα το Person.getThoseInCommon()
μέθοδος, κάνοντας το Person.Java
τάξη μοιάζει Αυτό .
Το testGetThoseInCommon()
, σε αντίθεση με το testPublishAge()
, δεν επαληθεύει ότι πραγματοποιήθηκαν συγκεκριμένες κλήσεις προς birthdaysClient
μεθόδους. Αντ 'αυτού χρησιμοποιεί when
κλήσεις προς στέλεχος επιστρέφουν τιμές για κλήσεις προς findFamousNamesOfAge()
και findFamousNamesBornOn()
ότι getThoseInCommon()
θα πρέπει να κάνει. Στη συνέχεια ισχυριζόμαστε ότι και τα τρία από τα μαχαιρωμένα ονόματα που δώσαμε επιστρέφονται.
Αναδίπλωση πολλαπλών ισχυρισμών με το assertAll()
Η μέθοδος JUnit 5 επιτρέπει τον έλεγχο όλων των ισχυρισμών στο σύνολό του, αντί να σταματήσει μετά τον πρώτο αποτυχημένο ισχυρισμό. Περιλαμβάνουμε επίσης ένα μήνυμα με assertTrue()
για να προσδιορίσετε συγκεκριμένα ονόματα που δεν περιλαμβάνονται. Δείτε πώς μοιάζει η μέθοδος δοκιμής «ευτυχισμένος δρόμος» (ένα ιδανικό σενάριο) (σημειώστε ότι αυτό δεν είναι ένα ισχυρό σύνολο δοκιμών από τη φύση του ότι είναι «ευτυχισμένος δρόμος», αλλά θα μιλήσουμε για το γιατί αργότερα.
// ... class PersonTest { // ... @Test void testGetThoseInCommon() throws IOException, PersonException { LocalDate dateOfBirth = LocalDate.parse('2000-01-02'); LocalDate currentDate = LocalDate.parse('2017-01-01'); Person person = new Person('Joe', 'Sixteen', dateOfBirth, ()->currentDate, birthdaysClient); when(birthdaysClient.findFamousNamesOfAge(16)).thenReturn(Arrays.asList('JoeFamous Sixteen', 'Another Person')); when(birthdaysClient.findFamousNamesBornOn(1, 2)).thenReturn(Arrays.asList('Jan TwoKnown')); Set thoseInCommon = person.getThoseInCommon(); assertAll( setContains(thoseInCommon, 'Another Person'), setContains(thoseInCommon, 'Jan TwoKnown'), setContains(thoseInCommon, 'JoeFamous Sixteen'), ()-> assertEquals(3, thoseInCommon.size()) ); } private Executable setContains(Set set, T expected) { return () -> assertTrue(set.contains(expected), 'Should contain ' + expected); } // ... }
Παρόλο που συχνά παραβλέπεται, είναι εξίσου σημαντικό να διατηρείτε τον κώδικα δοκιμής απαλλαγμένο από επικαλύψεις. Καθαρίστε τον κώδικα και τις αρχές όπως 'Μην επαναλαμβάνεις τον εαυτό σου' είναι πολύ σημαντικά για τη διατήρηση κωδικού βάσης υψηλής ποιότητας, παραγωγής και δοκιμαστικού κώδικα. Σημειώστε ότι το πιο πρόσφατο PersonTest.java έχει κάποια επανάληψη τώρα που έχουμε αρκετές μεθόδους δοκιμής.
Για να το διορθώσουμε, μπορούμε να κάνουμε μερικά πράγματα:
Εξαγάγετε το αντικείμενο IOException σε ένα ιδιωτικό τελικό πεδίο.
Εξαγάγετε το Person
δημιουργία αντικειμένων στη δική της μέθοδο (createJoeSixteenJan2()
, σε αυτήν την περίπτωση) αφού τα περισσότερα αντικείμενα του Person δημιουργούνται με τις ίδιες παραμέτρους.
Δημιουργήστε ένα assertCauseAndMessage()
για τις διάφορες δοκιμές που επιβεβαιώνουν τη ρίψη PersonExceptions
.
Τα αποτελέσματα του καθαρού κώδικα μπορούν να δουν σε αυτήν την απόδοση του PersonTest.java αρχείο.
Τι πρέπει να κάνουμε όταν ένα Person
αντικείμενο έχει ημερομηνία γέννησης που είναι μεταγενέστερη από την τρέχουσα ημερομηνία; Τα ελαττώματα στις εφαρμογές οφείλονται συχνά σε απροσδόκητη είσοδο ή έλλειψη προοπτικής σε περιπτώσεις γωνιών, άκρων ή ορίων. Είναι σημαντικό να προσπαθήσουμε να προβλέψουμε αυτές τις καταστάσεις όσο καλύτερα μπορούμε, και οι δοκιμές μονάδων είναι συχνά το κατάλληλο μέρος για να το κάνουμε. Κατασκευάζοντας το Person
και PersonTest
, συμπεριλάβαμε μερικές δοκιμές για αναμενόμενες εξαιρέσεις, αλλά δεν ήταν καθόλου πλήρης. Για παράδειγμα, χρησιμοποιούμε LocalDate
που δεν αντιπροσωπεύει ή αποθηκεύει δεδομένα ζώνης ώρας. Οι κλήσεις μας προς LocalDate.now()
, ωστόσο, επιστρέφουν ένα LocalDate
με βάση την προεπιλεγμένη ζώνη ώρας του συστήματος, η οποία θα μπορούσε να είναι μια ημέρα νωρίτερα ή αργότερα από εκείνη του χρήστη ενός συστήματος. Αυτοί οι παράγοντες πρέπει να λαμβάνονται υπόψη με την εφαρμογή κατάλληλων δοκιμών και συμπεριφοράς.
Τα όρια πρέπει επίσης να δοκιμαστούν. Εξετάστε ένα Person
αντικείμενο με ένα getDaysUntilBirthday()
μέθοδος. Οι δοκιμές θα πρέπει να περιλαμβάνουν εάν τα γενέθλια του ατόμου έχουν ήδη περάσει κατά τη διάρκεια του τρέχοντος έτους, αν τα γενέθλια του ατόμου είναι σήμερα και πώς ένα έτος άλματος επηρεάζει τον αριθμό των ημερών. Αυτά τα σενάρια μπορούν να καλυφθούν ελέγχοντας μία ημέρα πριν από τα γενέθλια του ατόμου, την ημέρα και μία ημέρα μετά τα γενέθλια του ατόμου όπου το επόμενο έτος είναι ένα άλμα. Εδώ είναι ο σχετικός κωδικός δοκιμής:
// ... class PersonTest { private final Supplier currentDateSupplier = ()-> LocalDate.parse('2015-05-02'); private final LocalDate ageJustOver5 = LocalDate.parse('2010-05-01'); private final LocalDate ageExactly5 = LocalDate.parse('2010-05-02'); private final LocalDate ageAlmost5 = LocalDate.parse('2010-05-03'); // ... @Test void testGetDaysUntilBirthday() { assertAll( createPersonAndAssertValue(ageAlmost5, 1, Person::getDaysUntilBirthday), createPersonAndAssertValue(ageExactly5, 0, Person::getDaysUntilBirthday), createPersonAndAssertValue(ageJustOver5, 365, Person::getDaysUntilBirthday) ); } private Executable createPersonAndAssertValue(LocalDate dateOfBirth, long expectedValue, Function personLongFunction) { Person person = new Person('Given', 'Sur', dateOfBirth, currentDateSupplier); long actualValue = personLongFunction.apply(person); return () -> assertEquals(expectedValue, actualValue); } }
Έχουμε επικεντρωθεί κυρίως σε δοκιμές μονάδας, αλλά το JUnit μπορεί επίσης να χρησιμοποιηθεί για δοκιμές ολοκλήρωσης, αποδοχής, λειτουργίας και συστήματος. Τέτοιες δοκιμές συχνά απαιτούν περισσότερο κώδικα ρύθμισης, π.χ. εκκίνηση διακομιστών, φόρτωση βάσεων δεδομένων με γνωστά δεδομένα κ.λπ. Ενώ μπορούμε συχνά να εκτελέσουμε χιλιάδες δοκιμές μονάδας σε δευτερόλεπτα, οι μεγάλες σουίτες δοκιμών ενοποίησης μπορεί να διαρκέσουν λεπτά ή και ώρες για να εκτελεστούν. Γενικά, οι δοκιμές ολοκλήρωσης δεν πρέπει να χρησιμοποιούνται για να προσπαθήσουν να καλύψουν κάθε αλλαγή ή διαδρομή μέσω του κώδικα. οι δοκιμές μονάδας είναι πιο κατάλληλες για αυτό.
Η δημιουργία δοκιμών για εφαρμογές ιστού που οδηγούν προγράμματα περιήγησης ιστού σε συμπλήρωση φορμών, κάνοντας κλικ σε κουμπιά, αναμονή φόρτωσης περιεχομένου κ.λπ., γίνεται συνήθως χρησιμοποιώντας Selenium WebDriver (Άδεια Apache 2.0) σε συνδυασμό με το 'Πρότυπο αντικειμένου σελίδας' (βλ SeleniumHQ github wiki και Το άρθρο του Martin Fowler σχετικά με τα αντικείμενα σελίδας ).
Το JUnit είναι αποτελεσματικό για τον έλεγχο RESTful API με τη χρήση ενός προγράμματος-πελάτη HTTP όπως το Apache HTTP Client ή το Spring Rest Template ( Το HowToDoInJava.com παρέχει ένα καλό παράδειγμα ).
Στην περίπτωσή μας με το Person
αντικείμενο, μια δοκιμή ενοποίησης θα μπορούσε να περιλαμβάνει τη χρήση του πραγματικού BirthdaysClient
παρά μια πλαστή, με μια διαμόρφωση που καθορίζει τη βασική διεύθυνση URL της υπηρεσίας People Birthdays. Στη συνέχεια, ένα τεστ ενοποίησης θα χρησιμοποιούσε μια δοκιμαστική παρουσία μιας τέτοιας υπηρεσίας, θα επαληθεύσει ότι τα γενέθλια δημοσιεύτηκαν σε αυτήν και θα δημιουργούσαν διάσημα άτομα στην υπηρεσία που θα επιστραφούν.
Το JUnit έχει πολλές επιπλέον δυνατότητες που δεν έχουμε ακόμη διερευνήσει στα παραδείγματα. Θα περιγράψουμε μερικά και θα παρέχουμε αναφορές για άλλους.
Πρέπει να σημειωθεί ότι το JUnit δημιουργεί μια νέα παρουσία της τάξης δοκιμής για την εκτέλεση κάθε @Test
μέθοδος. Το JUnit παρέχει επίσης άγκιστρα σχολιασμού για την εκτέλεση συγκεκριμένων μεθόδων πριν ή μετά από όλα ή καθένα από τα @Test
μεθόδους. Αυτά τα άγκιστρα χρησιμοποιούνται συχνά για τη ρύθμιση ή τον καθαρισμό βάσεων δεδομένων ή πλαστών αντικειμένων και διαφέρουν μεταξύ JUnit 4 και 5.
JUnit 4 | JUnit 5 | Για μια στατική μέθοδο; |
---|---|---|
@BeforeClass | @BeforeAll | Ναί |
@AfterClass | @AfterAll | Ναί |
@Before | @BeforeEach | Οχι |
@After | @AfterEach | Οχι |
Στο PersonTest
μας Για παράδειγμα, επιλέξαμε να διαμορφώσουμε το BirthdaysClient
πλαστό αντικείμενο στο @Test
οι ίδιες μέθοδοι, αλλά μερικές φορές πιο περίπλοκες πλαστές δομές πρέπει να δημιουργηθούν με πολλαπλά αντικείμενα. @BeforeEach
(σε JUnit 5) και @Before
(σε JUnit 4) είναι συχνά κατάλληλο για αυτό.
Το @After*
Οι σχολιασμοί είναι πιο συνηθισμένοι με τις δοκιμές ενοποίησης από τις δοκιμές μονάδας, καθώς η συλλογή απορριμμάτων JVM χειρίζεται τα περισσότερα αντικείμενα που δημιουργήθηκαν για δοκιμές μονάδας. Το @BeforeClass
και @BeforeAll
Οι σχολιασμοί χρησιμοποιούνται πιο συχνά για δοκιμές ενσωμάτωσης που πρέπει να εκτελούν δαπανηρές ενέργειες ρύθμισης και απογύμνωσης μία φορά, αντί για κάθε μέθοδο δοκιμής.
Για το JUnit 4, ανατρέξτε στο οδηγός φωτιστικών δοκιμών (οι γενικές έννοιες εξακολουθούν να ισχύουν για το JUnit 5).
Μερικές φορές θέλετε να εκτελέσετε πολλές σχετικές δοκιμές, αλλά όχι όλες τις δοκιμές. Σε αυτήν την περίπτωση, ομαδοποιήσεις δοκιμών μπορούν να αποτελούνται σε δοκιμαστικές σουίτες. Για να το κάνετε αυτό στο JUnit 5, ρίξτε μια ματιά Το άρθρο JUnit 5 του HowToProgram.xyz και στην ομάδα της JUnit τεκμηρίωση για το JUnit 4 .
Το JUnit 5 προσθέτει τη δυνατότητα χρήσης μη στατικών ένθετων εσωτερικών τάξεων για να δείξει καλύτερα τη σχέση μεταξύ δοκιμών. Αυτό θα πρέπει να είναι πολύ οικείο σε όσους έχουν εργαστεί με ένθετες περιγραφές σε δοκιμαστικά πλαίσια όπως το Jasmine για JavaScript. Οι εσωτερικές τάξεις σχολιάζονται με @Nested
να το χρησιμοποιήσω.
Παράδειγμα κόμβου js σε πραγματικό χρόνο
Το @DisplayName
Ο σχολιασμός είναι επίσης νέος στο JUnit 5, επιτρέποντάς σας να περιγράψετε τη δοκιμή για αναφορά σε μορφή συμβολοσειράς, που θα εμφανίζεται εκτός από το αναγνωριστικό μεθόδου δοκιμής.
Αν και @Nested
και @DisplayName
μπορούν να χρησιμοποιηθούν ανεξάρτητα το ένα από το άλλο, μαζί μπορούν να παρέχουν σαφέστερα αποτελέσματα δοκιμών που περιγράφουν τη συμπεριφορά του συστήματος.
ο Πλαίσιο Hamcrest , αν και δεν αποτελεί μέρος της βάσης κώδικα JUnit, παρέχει μια εναλλακτική λύση στη χρήση παραδοσιακών μεθόδων διεκδίκησης σε δοκιμές, επιτρέποντας έναν πιο εκφραστικό και αναγνώσιμο κώδικα δοκιμής. Δείτε την ακόλουθη επαλήθευση χρησιμοποιώντας ένα παραδοσιακό assertEquals και ένα Hamcrest assertThat:
//Traditional assert assertEquals('Hayden, Josh', displayName); //Hamcrest assert assertThat(displayName, equalTo('Hayden, Josh'));
Το Hamcrest μπορεί να χρησιμοποιηθεί και με τα JUnit 4 και 5. Το σεμινάριο του Vogella.com για το Hamcrest είναι αρκετά περιεκτικό.
Το άρθρο Δοκιμές μονάδας, πώς να γράψετε έναν ελεγχόμενο κώδικα και γιατί έχει σημασία καλύπτει πιο συγκεκριμένα παραδείγματα γραφής καθαρού, ελεγχόμενου κώδικα.
Δημιουργία με αυτοπεποίθηση: Ένας οδηγός για τις δοκιμές JUnit εξετάζει διαφορετικές προσεγγίσεις για τη δοκιμή ενοτήτων και ενοποίησης και γιατί είναι καλύτερο να επιλέξετε μία και να τηρήσετε
ο JUnit 4 Wiki και Οδηγός χρήσης JUnit 5 είναι πάντα ένα εξαιρετικό σημείο αναφοράς.
ο Τεκμηρίωση Mockito παρέχει πληροφορίες για πρόσθετες λειτουργίες και παραδείγματα.
Έχουμε διερευνήσει πολλές πτυχές των δοκιμών στον κόσμο της Java με το JUnit. Έχουμε εξετάσει τις δοκιμές μονάδας και ενοποίησης χρησιμοποιώντας το πλαίσιο JUnit για βάσεις κώδικα Java, ενσωματώνοντας το JUnit σε περιβάλλοντα ανάπτυξης και κατασκευής, πώς να χρησιμοποιούμε κωμίδες και stubs με προμηθευτές και Mockito, κοινές συμβάσεις και βέλτιστες πρακτικές κώδικα, τι να δοκιμάσουμε και μερικές άλλες εξαιρετικές δυνατότητες του JUnit.
Είναι τώρα η σειρά του αναγνώστη να αναπτυχθεί με επιδεξιότητα στην εφαρμογή, τη διατήρηση και την αξιοποίηση των πλεονεκτημάτων των αυτοματοποιημένων δοκιμών χρησιμοποιώντας το πλαίσιο JUnit.