Zilele sunt numărate pentru versiunea a 4-a a librăriei JUnit. Cel mai cuprinzător framework de testare în Java va fi îmbunătăţit în curând și ceea ce urmează îl face cu adevărat performant. JUnit 5 este mai mult decât o librărie, este o platformă pentru testare. Nu doar că rezolvă problemele versiunilor anterioare oferind o interfaţă îmbunătăţită, dar introduce un mecanism nou pentru executarea și descoperirea testelor. Arhitectura stratificată asigură o separare a componentelor și permite conectarea a diferite motoare de executare pentru a menţine compatibilitatea cu versiunile anterioare. Lansarea oficială este planificată pentru primul trimestru al anului 2017, dar până atunci să aruncăm o privire și să identificam motivele pentru care poate fi un candidat bun pentru unit-testing în proiectul următor.
În prima parte, vom urmări cum se scriu testele și vom identifica îmbunătăţirile pe care le aduce versiunea a 5-a și vom încheia prin a înţelege motivele pentru care noua arhitectură este inovatoare.
Majoritatea adnotărilor sunt redenumite, iar clasele se pot găsi în pachetul org.junit.juniper.api.
JUnit 4 | JUnit 5 |
---|---|
@Before | @BeforeEach |
@After | @AfterEach |
@BeforeClass | @BeforeAll |
@AfterClass | @AfterAll |
@Ignored | @Disabled("reason") |
@RunWith | @ExtendWith |
Clasele de aserţiuni au fost mutate din pachetul org.junit.Assert în org.junit.jupiter.api. Assertions și au fost aduse trei îmbunătăţiri.
a) Mesajul în cazul unei erori a fost mutat ultimul în lista de parametri.
assertEquals(expected, actual, "error message");
b) Expresii lambda
assertTrue(false, () -> "Error. Expected true");
Exemplul precedent este trivial, dar expresiile lambda fiind evaluate cu întârziere economisesc resurse și timp în cazul în care logica de construcție a mesajului este costisitoare.
c) Asețiuni multiple
assertAll("Person",
() -> assertEquals("John", person.getFirstName()),
() -> assertEquals("Doe", person.getSurname()),
() -> assertEquals("SW19 5HP", person.getPostCode()));
Această îmbunătăţire este foarte utilă deoarece în precedentele versiuni ale librăriei, execuţia se opreşte după prima evaluare eșuată și nu oferă un rezultat final al aserțiunilor din test. În cazul unei erori, utilizatorului îi va fi afișat următorul mesaj:
AssertionsTest.multipleAssertionsTest:24 Person (2 failures)
expected: <Doe> but was: <Toe>
expected: <SW19 5HP> but was: <NW19 3RU>
Testele pot fi etichetate cu adnotarea @Tag
și dezactivate cu @Disabled
adăugate la nivelul clasei sau metodei.
Acestea sunt folosite în situaţiile în care aserţiunile trebuie evaluate doar dacă anumite criterii sunt îndeplinite:
@Test
void shouldEvaluateSeniors() {
assumeTrue(person.getAge > 65);
assertAll("Person",
() -> assertEquals("John", person.getFirstName()),
() -> assertEquals("Doe", person.getSurname()));
}
Testarea excepțiilor a fost ignorată de cele mai multe ori sau a fost realizată superficial. Vestea bună este aceea că JUnit 5 permite o testare facilă a tipului și a mesajului excepției.
@Test
void testException() {
Throwable e = assertThrows(RuntimeException.class, () -> target.throwEx());
assertEquals("message", e.getMessage());
}
Librăria JUnit 5 introduce testele imbricate pentru a exprima relațiile complexe între diferite grupuri de teste. De asemenea, permite dezvoltarea într-un stil BDD prin adnotarea testelor cu @DisplayName
și atribuirea unui nume lizibil. Clasele imbricate sunt marcate cu adnotarea @Nested
pentru a permite integrarea corectă a acestora. Documentaţia oficială prezintă un exemplu foarte bun de astfel de teste.
Testele dezvoltate în JUnit 4 sunt statice, în sensul în care acestea sunt definite complet în momentul compilării. Ideea nouă care a fost adusă în ultima versiune introduce un nou tip de test, cel dinamic, care este generat la runtime de către o metodă de tip factory adnotată cu @TestFactory
. Această metodă nu reprezintă un test în adevăratul sens al cuvântului, dar este folosită pentru a genera în mod dinamic testele și poate returna Stream
, Collection
sau Iterable
de instanțe ale clasei DynamicTest
. Această clasă este @FunctionalInterface
ceea ce înseamnă că implementarea testului poate fi creată printr-o expresie lambda.
@TestFactory
Collection<DynamicTest> generateDynamicTests() {
return Arrays.asList(
DynamicTest.dynamicTest("Test status", () -> assertTrue(p.isMarried())),
DynamicTest.dynamicTest("Test age", () -> assertEquals(30, p.getAge()))
);
}
Avantajul testării dinamice apare când se adoptă testarea data driven.
Acest tip de testare permite excutarea de multiple iterații ale aceluiași test cu date de intrare diferite pentru a evita duplicarea codului. Această procedură se numește table driven testing în alte librării de testare sau limbaje de programare precum Go, Spock sau Cucumber. În JUnit 5 putem implementa astfel de teste cu ajutorul metodelor de tip factory prin parcurgerea unei colecții de date de test și crearea dinamică a testelor pentru fiecare set de date.
@TestFactory
Collection<DynamicTest> generateDynamicTests() {
return testData()
.stream()
.map(d -> dynamicTest("Data:" + d, () -> {
/** implementation */
}))
.collect(Collectors.toList());
}
După cum ne spune și numele, această funcționalitate permite programatorilor să administreze ciclul de viaţă al testelor și să extindă logica acestora fără a modifica funcționalitatea existentă. Acest lucru poate fi implementat cu așa numitele puncte de extensie (extension points) care sunt oferite în mod implicit. Din punct de vedere tehnic, acestea sunt interfețe în a căror implementare se definește logica de extensie. Implementările se numesc extensii și acestea pot fi înregistrate folosind adnotarea @ExtendWith
la nivelul clasei sau metodei extinse.
Pentru a folosi extensiile trebuie să importăm pachetul org.junit.jupiter.api.extension
.
Punct de extensie | Rol |
---|---|
TestInstancePostProcessor |
Definește logica de postprocesare a |
testului. | |
ContainerExecutionCondition |
Definește interfața pentru execuția |
condiționată a containerului. | |
ParameterResolver |
Definește interfața pentru rezolvarea |
dinamică a parametrilor la runtime. | |
TestExecutionExceptionHandler |
Definește interfața pentru a manipula |
excepțiile din timpul execuției | |
testelor. | |
BeforeAllCallback , BeforeEachCallback , |
Definesc interfețe pentru administrarea |
AfterAllCallback , AfterEachCallback |
ciclului de viață al testelor. |
JUnit 4 a fost lansat în urmă cu aproximativ 12 ani. De atunci, numeroase lucruri au evoluat în domeniul programării de la Java și JVM pana la IDE-uri și instrumentele de CI. Testele au devenit din ce în ce mai importante în munca de zi cu zi a programatorilor, rezultatele rapide și navigarea facilă fiind elemente cheie ale unei librării performante. Mai mult decât atât, IDE-urile și instrumentele de CI au devenit dependente de detaliile de implementare ale librăriei JUnit pentru a se integra cu aceasta. JUnit 4.12 (ultima versiune) este un artefact monolitic care conține doar interfața pentru testare și motorul de executare al testelor. Mecanismul de descoperire al testelor trebuie implementat separat de către fiecare instrument. Johannes Link, unul dintre iniţiatorii noului proiect, menționa într-un interviu "succesul JUnit ca o platforma previne dezvoltarea JUnit ca un instrument de testare" ("the success of JUnit as a platform prevents the development of JUnit as a test tool"). Ceea ce noul proiect (denumit iniţial JUnit Lambda) a încercat sa rezolve a fost separarea interfeței publice de motorul de executare.
Noua platformă de testare are două părți importante:
interfața (API) pentru dezvoltarea testelor
Despre prima parte am discutat în introducerea articolului, iar în continuare vom detalia modul în care arhitectura a fost realizată. Aceasta este compusă din trei module, determinând astfel o arhitectură decuplată a noii platforme:
un motor pentru descoperirea și executare al testelor;
o interfaţă comună pe care toate motoarele trebuie să o implementeze pentru uniformitate;
Această abordare de decuplare conduce la arhitectura prezentată în Figura 1.
Figura 1 - Junit 5 Architecture
junit-juniper-api - interfața folosită de programatori pentru dezvoltarea testelor în JUnit 5;
junit-juniper-engine - implementare a junit-platform-engine care execută testele JUnit 5;
junit-vintage-engine - implementare a junit-platform-engine care execută testele JUnit 4;
junit-platform-engine - interfața comună pe care toate motoarele trebuie să o implementeze și să o utilizeze pentru a se înregistra la platforma de lansare (platform-launcher);
junit-platform-launcher - orchestrează motoarele, folosește ServiceLoader pentru descoperirea diferitelor implementări ale motorului (platform-engine) și oferă o interfață instrumentelor pentru a interacționa cu executarea testelor.
Diagrama precedentă poate fi sumarizată ca JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage. Trebuie menționat faptul că platforma oferă utilizatorului flexibilitatea de a folosi propriul motor de testare prin simpla implementare a interfețelor din artefactul junit-platform-engine și înregistrarea acestuia. Compatibilitatea cu versiunile anterioare (a 3-a și a 4-a) este realizată prin motorul JUnit Vintage, iar avantajul este că acesta poate exista în proiect simultan cu JUnit Juniper.
Ca o notă de final, pentru o inspectare a acestei platforme trebuie incluse în fișierul de configurare artefactele junit-juniper-api în secțiunea de dependențe și junit-juniper-engine în secțiunea de plugin.
După cum am observat, s-a realizat un progres major în ultima versiune de JUnit. Cea mai folosită librărie pentru testare în ecosistemul JVM a fost suferit un upgrade serios și a făcut un pas important spre a deveni o platformă de testare. Interfața îmbunătăţită a rezolvat principalele probleme din versiunile anterioare, iar arhitectura stratificată permite o separare clară a componentelor și chiar oferă fundamentele pentru folosirea unui motor de testare propriu.