Un ghid pentru teste de unitate și de integrare robuste cu JUnit

Publicat: 2022-03-11

Testele software automate sunt de o importanță critică pentru calitatea, mentenabilitatea și extensibilitatea pe termen lung a proiectelor software, iar pentru Java, JUnit este calea către automatizare.

În timp ce cea mai mare parte a acestui articol se va concentra pe scrierea de teste unitare robuste și pe utilizarea stubbing, batjocorirea și injecția de dependență, vom discuta și despre JUnit și testele de integrare.

Cadrul de testare JUnit este un instrument comun, gratuit și open source pentru testarea proiectelor bazate pe Java.

În momentul în care am scris acest articol, JUnit 4 este versiunea majoră actuală, fiind lansată cu mai bine de 10 ani în urmă, ultima actualizare având mai mult de doi ani în urmă.

JUnit 5 (cu modelele de programare și extensie Jupiter) este în dezvoltare activă. Acceptă mai bine funcțiile de limbaj introduse în Java 8 și include alte caracteristici noi, interesante. Unele echipe pot găsi JUnit 5 gata de utilizare, în timp ce altele pot continua să folosească JUnit 4 până când 5 este lansat oficial. Ne vom uita la exemple din ambele.

Rulează JUnit

Testele JUnit pot fi rulate direct în IntelliJ, dar pot fi rulate și în alte IDE-uri precum Eclipse, NetBeans sau chiar linia de comandă.

Testele ar trebui să ruleze întotdeauna în timpul construirii, în special testele unitare. O build cu orice teste eșuate ar trebui considerată eșuată, indiferent dacă problema este în producție sau în codul de testare - acest lucru necesită disciplină din partea echipei și dorința de a acorda cea mai mare prioritate rezolvării testelor eșuate, dar este necesar să se respecte spirit de automatizare.

Testele JUnit pot fi, de asemenea, rulate și raportate prin sisteme de integrare continuă precum Jenkins. Proiectele care folosesc instrumente precum Gradle, Maven sau Ant au avantajul suplimentar de a putea rula teste ca parte a procesului de construire.

Grupuri de viteze care indică compatibilitatea: JUnit 4 cu NetBeans într-unul, JUnit 5 cu Eclipse și Gradle în altul, iar ultimul având JUnit 5 cu Maven și IntelliJ IDEA.

Gradle

Ca exemplu de proiect Gradle pentru JUnit 5, consultați secțiunea Gradle din ghidul utilizatorului JUnit și depozitul junit5-samples.git. Rețineți că poate rula și teste care utilizează API-ul JUnit 4 (denumit „vintage” ).

Proiectul poate fi creat în IntelliJ prin opțiunea de meniu Fișier > Deschidere... > navigați la junit-gradle-consumer sub-directory > OK > Deschidere ca proiect > OK pentru a importa proiectul din Gradle.

Pentru Eclipse, pluginul Buildship Gradle poate fi instalat din Ajutor > Eclipse Marketplace... Proiectul poate fi apoi importat cu File > Import... > Gradle > Gradle Project > Next > Next > Navigați la junit-gradle-consumer > Next > Următorul > Terminați.

După configurarea proiectului Gradle în IntelliJ sau Eclipse, rularea sarcinii de build Gradle va include rularea tuturor testelor JUnit cu sarcina de test . Rețineți că testele pot fi sărite la execuțiile ulterioare ale build ului dacă nu s-au făcut modificări codului.

Pentru JUnit 4, vezi utilizarea JUnit cu wiki Gradle.

Maven

Pentru JUnit 5, consultați secțiunea Maven din ghidul utilizatorului și depozitul junit5-samples.git pentru un exemplu de proiect Maven. Acesta poate rula și teste de epocă (cele care folosesc API-ul JUnit 4).

În IntelliJ, utilizați File > Open... > navigați la junit-maven-consumer/pom.xml > OK > Open as Project. Testele pot fi apoi rulate din Maven Projects > junit5-maven-consumer > Lifecycle > Test.

În Eclipse, utilizați Fișier > Import... > Maven > Proiecte Maven existente > Următorul > Răsfoiți la directorul junit-maven-consumer > Cu pom.xml selectat > Terminare.

Testele pot fi executate rulând proiectul ca Maven build... > specificați scopul test > Run.

Pentru JUnit 4, vezi JUnit în depozitul Maven.

Medii de dezvoltare

Pe lângă rularea testelor prin instrumente de construcție precum Gradle sau Maven, multe IDE-uri pot rula direct teste JUnit.

IntelliJ IDEA

IntelliJ IDEA 2016.2 sau o versiune ulterioară este necesară pentru testele JUnit 5, în timp ce testele JUnit 4 ar trebui să funcționeze în versiunile IntelliJ mai vechi.

În sensul acestui articol, este posibil să doriți să creați un nou proiect în IntelliJ dintr-unul dintre depozitele mele GitHub ( JUnit5IntelliJ.git sau JUnit4IntelliJ.git), care includ toate fișierele din exemplul simplu de clasă Person și care utilizează sistemul încorporat. bibliotecile JUnit. Testul poate fi rulat cu Run > Run 'All Tests'. Testul poate fi rulat și în IntelliJ din clasa PersonTest .

Aceste depozite au fost create cu noi proiecte IntelliJ Java și construiesc structurile de directoare src/main/java/com/example și src/test/java/com/example . Directorul src/main/java a fost specificat ca dosar sursă, în timp ce src/test/java a fost specificat ca dosar sursă de testare. După crearea clasei PersonTest cu o metodă de testare adnotată cu @Test , aceasta poate eșua compilarea, caz în care IntelliJ oferă sugestia de a adăuga JUnit 4 sau JUnit 5 la calea clasei care poate fi încărcată din distribuția IntelliJ IDEA (vezi acestea răspunsuri pe Stack Overflow pentru mai multe detalii). În cele din urmă, a fost adăugată o configurație de rulare JUnit pentru toate testele.

Consultați, de asemenea, Ghidurile de testare IntelliJ.

Eclipsă

Un proiect Java gol din Eclipse nu va avea un director rădăcină de testare. Acesta a fost adăugat din Properties proiect > Java Build Path > Add Folder... > Create New Folder... > specificați numele folderului > Finish. Noul director va fi selectat ca folder sursă. Faceți clic pe OK în ambele dialoguri rămase.

Testele JUnit 4 pot fi create cu File > New > JUnit Test Case. Selectați „New JUnit 4 test” și folderul sursă nou creat pentru teste. Specificați o „clasă testată” și un „pachet”, asigurându-vă că pachetul se potrivește cu clasa testată. Apoi, specificați un nume pentru clasa de testare. După ce ați terminat expertul, dacă vi se solicită, alegeți „Adăugați biblioteca JUnit 4” la calea de construire. Proiectul sau clasa de testare individuală poate fi apoi rulată ca un test JUnit. Vezi și Eclipse Writing and Running tests JUnit.

NetBeans

NetBeans acceptă doar testele JUnit 4. Clasele de testare pot fi create într-un proiect NetBeans Java cu File > New File... > Unit Tests > JUnit Test sau Test for Existing Class. În mod implicit, directorul rădăcină de testare este numit test în directorul proiectului.

Clasa de producție simplă și cazul său de testare JUnit

Să aruncăm o privire la un exemplu simplu de cod de producție și codul de test unitar corespunzător pentru o clasă Person foarte simplă. Puteți descărca exemplul de cod din proiectul meu github și îl puteți deschide prin IntelliJ.

src/main/java/com/example/Person.java
 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; } }

Clasa imuabilă Person are un constructor și o metodă getDisplayName() . Vrem să testăm că getDisplayName() returnează numele formatat așa cum ne așteptăm. Iată codul de testare pentru un singur test de unitate (JUnit 5):

src/test/java/com/example/PersonTest.java
 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 folosește @Test și afirmația JUnit 5. Pentru JUnit 4, clasa și metoda PersonTest trebuie să fie publice și ar trebui utilizate diferite importuri. Iată exemplul JUnit 4 Gist.

La rularea clasei PersonTest în IntelliJ, testul trece și indicatorii UI sunt verzi.

Convențiile comune ale JUnit

Denumire

Deși nu este necesar, folosim convenții comune în denumirea clasei de testare; în special, începem cu numele clasei care este testată ( Person ) și îi adăugăm „Test” ( PersonTest ). Numirea metodei de testare este similară, începând cu metoda testată ( getDisplayName() ) și adăugându-i „test” ( testGetDisplayName() ). Deși există multe alte convenții perfect acceptabile pentru denumirea metodelor de testare, este important să fii consecvent în cadrul echipei și al proiectului.

Nume în producție Nume în Testare
Persoană Test de persoană
getDisplayName() testDisplayName()

Pachete

De asemenea, folosim convenția de a crea clasa PersonTest cod de test în același pachet ( com.example ) ca și clasa Person a codului de producție. Dacă am folosi un pachet diferit pentru teste, ni se va cere să folosim modificatorul de acces public în clasele de cod de producție, constructori și metode la care se face referire de testele unitare, chiar și acolo unde nu este adecvat, deci este mai bine să le păstrăm în același pachet. . Cu toate acestea, folosim directoare sursă separate ( src/main/java și src/test/java ) deoarece, în general, nu dorim să includem cod de testare în versiunile de producție lansate.

Structură și adnotare

Adnotarea @Test (JUnit 4/5) îi spune lui JUnit să execute metoda testGetDisplayName() ca metodă de testare și să raporteze dacă trece sau nu. Atâta timp cât toate afirmațiile (dacă există) trec și nu sunt aruncate excepții, testul este considerat a fi promovat.

Codul nostru de testare urmează modelul de structură Arrange-Act-Assert (AAA). Alte modele comune includ Given-When-Then și Setup-Exercise-Verify-Teardown (Demontarea nu este de obicei necesară în mod explicit pentru testele unitare), dar folosim AAA în acest articol.

Să aruncăm o privire la modul în care exemplul nostru de test urmează AAA. Prima linie, „aranja” creează un obiect Person care va fi testat:

 Person person = new Person("Josh", "Hayden");

A doua linie, „act”, exercită metoda Person.getDisplayName() a codului de producție:

 String displayName = person.getDisplayName();

A treia linie, „afirmarea”, verifică dacă rezultatul este cel așteptat.

 assertEquals("Hayden, Josh", displayName);

Intern, apelul assertEquals() folosește metoda equals a obiectului String „Hayden, Josh” pentru a verifica valoarea reală returnată din codul de producție ( displayName ) potriviri. Dacă nu se potrivea, testul ar fi fost marcat ca eșuat.

Rețineți că testele au adesea mai mult de o linie pentru fiecare dintre aceste faze AAA.

Teste unitare și cod de producție

Acum că am acoperit câteva convenții de testare, să ne îndreptăm atenția spre a face codul de producție testabil.

Ne întoarcem la clasa noastră Person , unde am implementat o metodă de a returna vârsta unei persoane pe baza datei sale de naștere. Exemplele de cod necesită Java 8 pentru a profita de noile date și API-uri funcționale. Iată cum arată noua clasă Person.java :

Persoană.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 } }

Conducerea acestei clase (la momentul scrierii) anunță că Joey are 4 ani. Să adăugăm o metodă de testare:

PersonTest.java
 // ... class PersonTest { // ... @Test void testGetAge() { Person person = new Person("Joey", "Doe", LocalDate.parse("2013-01-12")); long age = person.getAge(); assertEquals(4, age); } }

Trece azi, dar ce zici când îl vom rula peste un an? Acest test este nedeterminist și fragil, deoarece rezultatul așteptat depinde de data curentă a sistemului care rulează testul.

Îndepărtarea și injectarea unui furnizor de valoare

Când rulăm în producție, dorim să folosim data curentă, LocalDate.now() , pentru calcularea vârstei persoanei, dar pentru a face un test determinist chiar și peste un an, testele trebuie să furnizeze propriile valori currentDate .

Aceasta este cunoscută sub numele de injecție de dependență. Nu dorim ca obiectul nostru Person să determine data curentă în sine, ci în schimb dorim să trecem această logică ca o dependență. Testele unitare vor folosi o valoare cunoscută, blocată, iar codul de producție va permite ca valoarea reală să fie furnizată de sistem în timpul execuției.

Să adăugăm un furnizor LocalDate la Person.java :

Persoană.java
 // ... class Person { // ... private final LocalDate dateOfBirth; private final Supplier<LocalDate> 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<LocalDate> 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 } }

Pentru a facilita testarea getAge() , am schimbat-o pentru a folosi currentDateSupplier , un furnizor LocalDate , pentru a prelua data curentă. Dacă nu știți ce este un furnizor, vă recomand să citiți despre interfețele funcționale încorporate Lambda.

Am adăugat și o injecție de dependență: noul constructor de testare permite testelor să furnizeze propriile valori ale datei curente. Constructorul inițial apelează acest nou constructor, trecând o referință de metodă statică a LocalDate::now , care furnizează un obiect LocalDate , astfel încât metoda noastră principală funcționează ca înainte. Dar metoda noastră de testare? Să actualizăm PersonTest.java :

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); } }

Testul își injectează acum propria valoare currentDate , astfel încât testul nostru va trece în continuare atunci când este rulat anul viitor sau în orice an. Acest lucru este denumit în mod obișnuit stubbing , sau furnizarea unei valori cunoscute care trebuie returnată, dar mai întâi a trebuit să schimbăm Person pentru a permite această dependență să fie injectată.

Observați sintaxa lambda ( ()->currentDate ) când construiți obiectul Person . Acesta este tratat ca un furnizor al unui LocalDate , așa cum este cerut de noul constructor.

Batjocorirea și împingerea unui serviciu web

Suntem pregătiți ca obiectul nostru Person – a cărui întreagă existență a fost în memoria JVM – să comunice cu lumea exterioară. Dorim să adăugăm două metode: metoda publishAge() , care va posta vârsta curentă a persoanei și metoda getThoseInCommon() , care va returna numele unor persoane celebre care au aceeași zi de naștere sau au aceeași vârstă cu Person noastră. Să presupunem că există un serviciu RESTful cu care putem interacționa numit „Zile de naștere ale oamenilor”. Avem un client Java pentru acesta, care constă dintr-o singură clasă, BirthdaysClient .

com.example.birthdays.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<String> 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<String> 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 */); } }

Să ne îmbunătățim clasa Person . Începem prin a adăuga o nouă metodă de testare pentru comportamentul dorit al publishAge() . De ce să începem cu testul, mai degrabă decât cu funcționalitatea? Urmăm principiile dezvoltării bazate pe teste (cunoscute și sub numele de TDD), în care scriem mai întâi testul și apoi codul pentru a-l face să treacă.

PersonTest.java
 // … 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(); } }

În acest moment, codul de testare nu reușește să se compileze deoarece nu am creat metoda publishAge() pe care o apelează. Odată ce creăm o metodă goală Person.publishAge() , totul trece. Acum suntem pregătiți pentru testul pentru a verifica dacă vârsta persoanei este de fapt publicată în BirthdaysClient .

Adăugarea unui obiect batjocorit

Deoarece acesta este un test unitar, ar trebui să ruleze rapid și în memorie, astfel încât testul va construi obiectul nostru Person cu un simulat BirthdaysClient , astfel încât să nu facă de fapt o solicitare web. Testul va folosi apoi acest obiect simulat pentru a verifica dacă a fost apelat conform așteptărilor. Pentru a face acest lucru, vom adăuga o dependență de cadrul Mockito (licență MIT) pentru a crea obiecte simulate, apoi vom crea un obiect BirthdaysClient batjocorit:

PersonTest.java
 // ... 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); // ... } }

În plus, am mărit semnătura constructorului Person pentru a prelua un obiect BirthdaysClient și am schimbat testul pentru a injecta obiectul BirthdaysClient batjocorit.

Adăugarea unei așteptări simulate

Apoi, adăugăm la sfârșitul testPublishAge o așteptare ca BirthdaysClient să fie numit. Person.publishAge() ar trebui să îl numească, așa cum se arată în noul nostru PersonTest.java :

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); } }

BirthdaysClient îmbunătățit cu Mockito ține evidența tuturor apelurilor care au fost efectuate către metodele sale, așa cum verificăm că nu au fost efectuate apeluri către BirthdaysClient cu metoda verifyZeroInteractions() înainte de a apela publishAge() . Deși probabil că nu este necesar, făcând acest lucru ne asigurăm că constructorul nu face apeluri necinstite. Pe linia verify() , specificăm cum ne așteptăm să arate apelul către BirthdaysClient .

Rețineți că, deoarece publishRegularPersonAge are IOException în semnătură, o adăugăm și la semnătura metodei noastre de testare.

În acest moment, testul eșuează:

 Wanted but not invoked: birthdaysClient.publishRegularPersonAge( "Joe Sixteen", 16L ); -> at com.example.PersonTest.testPublishAge(PersonTest.java:40)

Acest lucru este de așteptat, având în vedere că încă nu am implementat modificările necesare în Person.java , deoarece urmăm dezvoltarea bazată pe teste. Acum vom trece acest test făcând modificările necesare:

Persoană.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<LocalDate> 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(); } } }

Testarea pentru excepții

Am făcut ca constructorul de cod de producție să instanțieze un nou BirthdaysClient , iar publishAge() apelează acum birthdaysClient . Toate testele trec; totul este verde. Grozav! Dar observați că publishAge() înghite IOException. În loc să-l lăsăm să iasă, vrem să-l împachetăm cu propria noastră PersonException într-un fișier nou numit PersonException.java :

PersonException.java
 package com.example; public class PersonException extends Exception { public PersonException(String message, Throwable cause) { super(message, cause); } }

Implementăm acest scenariu ca o nouă metodă de testare în PersonTest.java :

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()); } } }

Apelul Mockito doThrow() stubs birthdaysClient pentru a arunca o excepție atunci când este apelată metoda publishRegularPersonAge() . Dacă PersonException nu este aruncată, pisăm testul. În caz contrar, afirmăm că excepția a fost legată în mod corespunzător cu IOException și verificăm că mesajul de excepție este așa cum era de așteptat. Momentan, deoarece nu am implementat nicio manipulare în codul nostru de producție, testul nostru eșuează deoarece excepția așteptată nu a fost aruncată. Iată ce trebuie să schimbăm în Person.java pentru a trece testul:

Persoană.java
 // ... class Person { // ... void publishAge() throws PersonException { // ... try { // ... } catch (IOException e) { throw new PersonException("Failed to publish " + nameToPublish + " age " + age, e); } } }

Stubs: Whens and Assertions

Acum implementăm metoda Person.getThoseInCommon() , făcând clasa noastră Person.Java să arate astfel.

testGetThoseInCommon() , spre deosebire testPublishAge() , nu verifică dacă anumite apeluri au fost efectuate către metodele birthdaysClient . În schimb, folosește when apelurile la stub returnează valori pentru apelurile la findFamousNamesOfAge() și findFamousNamesBornOn() pe care getThoseInCommon() va trebui să le facă. Apoi afirmăm că toate cele trei nume pe care le-am furnizat sunt returnate.

Încheierea mai multor aserțiuni cu metoda assertAll() JUnit 5 permite ca toate aserțiunile să fie verificate ca întreg, mai degrabă decât să se oprească după prima afirmație eșuată. Includem, de asemenea, un mesaj cu assertTrue() pentru a identifica anumite nume care nu sunt incluse. Iată cum arată metoda noastră de testare „cale fericită” (un scenariu ideal) (rețineți că acesta nu este un set robust de teste prin natura căreia este „cale fericită", dar vom vorbi despre ce mai târziu.

PersonTest.java
 // ... 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<String> thoseInCommon = person.getThoseInCommon(); assertAll( setContains(thoseInCommon, "Another Person"), setContains(thoseInCommon, "Jan TwoKnown"), setContains(thoseInCommon, "JoeFamous Sixteen"), ()-> assertEquals(3, thoseInCommon.size()) ); } private <T> Executable setContains(Set<T> set, T expected) { return () -> assertTrue(set.contains(expected), "Should contain " + expected); } // ... }

Păstrați codul de testare curat

Deși deseori trecut cu vederea, este la fel de important să păstrați codul de testare fără duplicare. Codul curat și principiile precum „nu te repeta” sunt foarte importante pentru menținerea unei baze de cod de înaltă calitate, a codului de producție și de testare deopotrivă. Observați că cel mai recent PersonTest.java are o oarecare duplicare acum că avem mai multe metode de testare.

Pentru a remedia acest lucru, putem face câteva lucruri:

  • Extrageți obiectul IOException într-un câmp final privat.

  • Extrageți crearea obiectului Person în propria sa metodă ( createJoeSixteenJan2() , în acest caz), deoarece majoritatea obiectelor Person sunt create cu aceiași parametri.

  • Creați un assertCauseAndMessage() pentru diferitele teste care verifică PersonExceptions aruncate.

Rezultatele codului curat pot fi văzute în această redare a fișierului PersonTest.java.

Testează mai mult decât calea fericită

Ce ar trebui să facem atunci când un obiect Person are o dată de naștere care este ulterioară datei curente? Defectele aplicațiilor se datorează adesea unei intrări neașteptate sau lipsei de previziune în cazurile de colț, margine sau limită. Este important să încercăm să anticipăm aceste situații cât mai bine putem, iar testele unitare sunt adesea un loc potrivit pentru a face acest lucru. În construirea Person și PersonTest , am inclus câteva teste pentru excepțiile așteptate, dar nu a fost în niciun caz complet. De exemplu, folosim LocalDate care nu reprezintă și nu stochează date de fus orar. Cu toate acestea, apelurile noastre către LocalDate.now() returnează un LocalDate bazat pe fusul orar implicit al sistemului, care ar putea fi cu o zi mai devreme sau mai târziu decât cel al utilizatorului unui sistem. Acești factori ar trebui luați în considerare cu teste și comportament adecvat implementate.

Granițele ar trebui, de asemenea, testate. Luați în considerare un obiect Person cu o metodă getDaysUntilBirthday() . Testarea ar trebui să includă dacă ziua de naștere a persoanei a trecut sau nu în anul curent, dacă ziua de naștere a persoanei este astăzi și modul în care un an bisect afectează numărul de zile. Aceste scenarii pot fi acoperite prin verificarea cu o zi înainte de ziua de naștere a persoanei, ziua și o zi după ziua de naștere a persoanei, în cazul în care anul următor este un an bisect. Iată codul de test pertinent:

PersonTest.java
 // ... class PersonTest { private final Supplier<LocalDate> 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<Person, Long> personLongFunction) { Person person = new Person("Given", "Sur", dateOfBirth, currentDateSupplier); long actualValue = personLongFunction.apply(person); return () -> assertEquals(expectedValue, actualValue); } }

Teste de integrare

Ne-am concentrat în mare parte pe testele unitare, dar JUnit poate fi folosit și pentru teste de integrare, acceptare, funcționale și de sistem. Astfel de teste necesită adesea mai mult cod de configurare, de exemplu, pornirea serverelor, încărcarea bazelor de date cu date cunoscute etc. Deși deseori putem rula mii de teste unitare în câteva secunde, rularea suitelor mari de teste pentru integrări poate dura minute sau chiar ore. În general, testele de integrare nu ar trebui folosite pentru a încerca să acopere fiecare permutare sau cale prin cod; testele unitare sunt mai potrivite pentru asta.

Crearea de teste pentru aplicațiile web care conduc browserele web în completarea formularelor, făcând clic pe butoane, așteptând încărcarea conținutului etc., se face de obicei folosind Selenium WebDriver (licență Apache 2.0) cuplată cu „Page Object Pattern” (vezi SeleniumHQ github wiki și articolul lui Martin Fowler despre obiectele paginii).

JUnit este eficient pentru testarea API-urilor RESTful cu utilizarea unui client HTTP, cum ar fi Apache HTTP Client sau Spring Rest Template (HowToDoInJava.com oferă un exemplu bun).

În cazul nostru, cu obiectul Person , un test de integrare ar putea implica utilizarea adevăratului BirthdaysClient , mai degrabă decât unul simulat, cu o configurație care specifică adresa URL de bază a serviciului People Birthdays. Un test de integrare ar folosi apoi o instanță de testare a unui astfel de serviciu, ar verifica dacă zilele de naștere au fost publicate și ar crea oameni celebri în serviciu care ar fi returnat.

Alte caracteristici JUnit

JUnit are multe caracteristici suplimentare pe care nu le-am explorat încă în exemple. Vom descrie unele și vom oferi referințe pentru altele.

Dispozitive de testare

Trebuie remarcat faptul că JUnit creează o nouă instanță a clasei de testare pentru rularea fiecărei metode @Test . JUnit oferă, de asemenea, cârlige de adnotare pentru a rula anumite metode înainte sau după toate sau fiecare dintre metodele @Test . Aceste cârlige sunt adesea folosite pentru configurarea sau curățarea bazei de date sau obiecte simulate și diferă între JUnit 4 și 5.

JU Unitatea 4 JU Unitatea 5 Pentru o metodă statică?
@BeforeClass @BeforeAll da
@AfterClass @AfterAll da
@Before @BeforeEach Nu
@After @AfterEach Nu

În exemplul nostru PersonTest , am ales să configuram obiectul simulat BirthdaysClient în metodele @Test în sine, dar uneori trebuie construite structuri simulate mai complexe care implică mai multe obiecte. @BeforeEach (în JUnit 5) și @Before (în JUnit 4) sunt adesea potrivite pentru acest lucru.

Adnotările @After* sunt mai frecvente la testele de integrare decât la testele unitare, deoarece colectarea de gunoi JVM gestionează majoritatea obiectelor create pentru testele unitare. @BeforeClass și @BeforeAll sunt cel mai frecvent utilizate pentru testele de integrare care trebuie să efectueze acțiuni costisitoare de configurare și demontare o dată, mai degrabă decât pentru fiecare metodă de testare.

Pentru JUnit 4, vă rugăm să consultați ghidul dispozitivelor de testare (conceptele generale încă se aplică pentru JUnit 5).

Suite de testare

Uneori doriți să rulați mai multe teste asociate, dar nu toate testele. În acest caz, grupările de teste pot fi compuse în suite de teste. Pentru cum să faceți acest lucru în JUnit 5, consultați articolul JUnit 5 al lui HowToProgram.xyz și în documentația echipei JUnit pentru JUnit 4.

@Nested și @DisplayName din JUnit 5

JUnit 5 adaugă capacitatea de a utiliza clase interioare imbricate non-statice pentru a arăta mai bine relația dintre teste. Acest lucru ar trebui să fie foarte familiar celor care au lucrat cu descrieri imbricate în cadre de testare precum Jasmine pentru JavaScript. Clasele interioare sunt adnotate cu @Nested pentru a utiliza acest lucru.

Adnotarea @DisplayName este, de asemenea, nouă pentru JUnit 5, permițându-vă să descrieți testul pentru raportare în format șir, care urmează să fie afișat în plus față de identificatorul metodei de testare.

Deși @Nested și @DisplayName pot fi utilizate independent unul de celălalt, împreună pot oferi rezultate mai clare ale testelor care descriu comportamentul sistemului.

Hamcrest Matchers

Cadrul Hamcrest, deși în sine nu face parte din baza de cod JUnit, oferă o alternativă la utilizarea metodelor tradiționale de afirmare în teste, permițând un cod de testare mai expresiv și mai lizibil. Vedeți următoarea verificare folosind atât un assertEquals tradițional, cât și un assertThat Hamcrest:

 //Traditional assert assertEquals("Hayden, Josh", displayName); //Hamcrest assert assertThat(displayName, equalTo("Hayden, Josh"));

Hamcrest poate fi folosit atât cu JUnit 4, cât și cu 5. Tutorialul Vogella.com despre Hamcrest este destul de cuprinzător.

Resurse aditionale

  • Articolul Teste unitare, Cum se scrie cod testabil și de ce este important acoperă exemple mai specifice de scriere a codului curat și testabil.

  • Construiți cu încredere: un ghid pentru testele JUnit examinează diferite abordări ale testării unitare și de integrare și de ce este mai bine să alegeți unul și să rămâneți cu el

  • Wiki JUnit 4 și Ghidul utilizatorului JUnit 5 sunt întotdeauna un punct de referință excelent.

  • Documentația Mockito oferă informații despre funcționalități suplimentare și exemple.

JUnit este calea către automatizare

Am explorat multe aspecte ale testării în lumea Java cu JUnit. Am analizat testele unitare și de integrare utilizând cadrul JUnit pentru bazele de cod Java, integrând JUnit în mediile de dezvoltare și construire, cum să folosim mock-uri și stub-uri cu furnizorii și Mockito, convențiile comune și cele mai bune practici de cod, ce trebuie testat și unele dintre alte caracteristici grozave ale JUnit.

Acum este rândul cititorului să crească în aplicarea, întreținerea și exploatarea cu pricepere a beneficiilor testelor automate folosind cadrul JUnit.