Vodič za parametarske testove JUnit 5

1. Pregled

JUnit 5, sljedeća generacija JUnit-a, olakšava pisanje testova za programere s novim i sjajnim značajkama.

Jedna od takvih karakteristika je strarameterizirani testovi. Ova nam značajka omogućuje izvršiti jednu ispitnu metodu više puta s različitim parametrima.

U ovom uputstvu detaljno ćemo istražiti parametrizirane testove, pa krenimo!

2. Ovisnosti

Da bismo koristili parametrizirane testove JUnit 5, moramo uvesti junit-jupiter-params artefakt s platforme JUnit. To znači da ćemo, kada koristimo Maven, dodati sljedeće pom.xml:

 org.junit.jupiter junit-jupiter-params 5.7.0 test 

Također, kada koristimo Gradle, odredit ćemo ga malo drugačije:

testCompile ("org.junit.jupiter: junit-jupiter-params: 5.7.0")

3. Prvi dojam

Recimo da imamo postojeću uslužnu funkciju i željeli bismo biti sigurni u njezino ponašanje:

brojevi javne klase {javni statički logički isOdd (int broj) {povratni broj% 2! = 0; }}

Parametarski testovi su poput ostalih testova, osim što dodamo @ParameterizedTest napomena:

@ParameterizedTest @ValueSource (ints = {1, 3, 5, -3, 15, Integer.MAX_VALUE}) // šest brojeva void isOdd_ShouldReturnTrueForOddNumbers (int number) {assertTrue (Numbers.isOdd (number)); }

JUnit 5 test trkač izvršava ovaj gornji test - i posljedično tome isOdd metoda - šest puta. I svaki put dodjeljuje vrijednost različitu od @ValueSource niz do broj parametar metode.

Dakle, ovaj primjer pokazuje nam dvije stvari koje su nam potrebne za parametarski test:

  • izvor argumenata, an int niz, u ovom slučaju
  • način da im se pristupi, u ovom slučaju, broj parametar

Također postoji još jedna stvar koja nije očita u ovom primjeru, pa budite u toku.

4. Izvori argumenata

Kao što bismo do sada trebali znati, parametarski test izvršava isti test više puta s različitim argumentima.

I, nadamo se, možemo i više od brojki - pa, istražimo!

4.1. Jednostavne vrijednosti

Uz @ValueSource napomena, metodu ispitivanja možemo proslijediti niz doslovnih vrijednosti.

Na primjer, pretpostavimo da ćemo testirati naše jednostavno isBlank metoda:

stringovi javne klase {javni statički logički isBlank (ulazni niz) return input == null}

Očekujemo da se ova metoda vrati pravi za null za prazne nizove. Dakle, možemo napisati parametarski test kao što je sljedeći kako bismo ustvrdili ovo ponašanje:

@ParameterizedTest @ValueSource (strings = {"", ""}) void isBlank_ShouldReturnTrueForNullOrBlankStrings (String input) {assertTrue (Strings.isBlank (input)); } 

Kao što vidimo, JUnit će pokrenuti ovaj test dva puta i svaki put parametru metode dodijeli jedan argument iz polja.

Jedno od ograničenja izvora vrijednosti jest da podržavaju samo sljedeće vrste:

  • kratak (s kratke hlače atribut)
  • bajt (s bajtova atribut)
  • int (s inti atribut)
  • dugo (s čezne atribut)
  • plutati (s pluta atribut)
  • dvostruko (s parovi atribut)
  • ugljen (s znakovi atribut)
  • java.lang.String (s žice atribut)
  • java.lang.Clasa (s razreda atribut)

Također, testnoj metodi možemo svaki put proslijediti samo jedan argument.

I prije nego što je krenuo dalje, je li netko primijetio da nismo prošli null kao argument? To je još jedno ograničenje: Ne možemo proći null kroz a @ValueSource, čak za Niz i Razred!

4.2. Nevaljane i prazne vrijednosti

Od JUnit-a 5.4 možemo proći jedan null vrijednost parametriziranoj ispitnoj metodi pomoću @NullSource:

@ParameterizedTest @NullSource void jeBlank_ShouldReturnTrueForNullInputs (String input) {assertTrue (Strings.isBlank (input)); }

Budući da primitivne vrste podataka ne mogu prihvatiti null vrijednosti, ne možemo koristiti @NullSource za primitivne argumente.

Potpuno slično, možemo proslijediti prazne vrijednosti pomoću @EmptySource napomena:

@ParameterizedTest @EmptySource void jeBlank_ShouldReturnTrueForEmptyStrings (String input) {assertTrue (Strings.isBlank (input)); }

@EmptySource prosljeđuje jedan prazan argument anotiranoj metodi.

Za Niz argumenata, proslijeđena vrijednost bila bi jednostavna kao prazna Niz. Štoviše, ovaj izvor parametara može pružiti prazne vrijednosti za Kolekcija vrste i nizovi.

Da bi prošli i jedno i drugo null i prazne vrijednosti, možemo koristiti sastavljeno @NullAndEmptySource napomena:

@ParameterizedTest @NullAndEmptySource void isBlank_ShouldReturnTrueForNullAndEmptyStrings (String input) {assertTrue (Strings.isBlank (input)); }

Kao i kod @EmptySource, sastavljena bilješka radi za Nizs,Kolekcijas, i nizova.

Da bi se prošlo još nekoliko praznih varijacija niza u parametriziranom testu, možemo kombinirati @ValueSource, @NullSource i @EmptySource zajedno:

@ParameterizedTest @NullAndEmptySource @ValueSource (strings = {"", "\ t", "\ n"}) void isBlank_ShouldReturnTrueForAllTypesOfBlankStrings (String input) {assertTrue (Strings.isBlank (input)); }

4.3. Enum

Da bismo pokrenuli test s različitim vrijednostima od nabrajanja, možemo koristiti @EnumSource bilješka.

Na primjer, možemo tvrditi da su svi mjesečni brojevi između 1 i 12:

@ParameterizedTest @EnumSource (Month.class) // prolazak svih 12 mjeseci void getValueForAMonth_IsAlwaysBetweenOneAndTwelve (mjesec u mjesecu) {int monthNumber = month.getValue (); assertTrue (monthNumber> = 1 && monthNumber <= 12); }

Ili možemo filtrirati nekoliko mjeseci pomoću imena atribut.

Što kažete na činjenicu da travanj, rujan, lipanj i studeni traju 30 dana:

@ParameterizedTest @EnumSource (value = Month.class, names = {"APRIL", "JUN", "SEPTEMBER", "NOVEMBER"}) void someMonths_Are30DaysLong (Month month) {final boolean isALeapYear = false; assertEquals (30, month.length (isALeapYear)); }

Prema zadanim postavkama imena zadržat će samo usklađene vrijednosti nabrajanja. To možemo preokrenuti postavljanjem način rada pripisati ISKLJUČITI:

@ParameterizedTest @EnumSource (value = Month.class, names = {"APRIL", "LIPANJ", "SEPTEMBAR", "STUDENI", "FEBRUAR"}, mode = EnumSource.Mode.EXCLUDE) void osimFourMonths_OthersAre31DaysLong {ong završni logički isALeapYear = false; assertEquals (31, month.length (isALeapYear)); }

Uz doslovne nizove, regularni izraz možemo proslijediti i na imena atribut:

@ParameterizedTest @EnumSource (value = Month.class, names = ". + BER", mode = EnumSource.Mode.MATCH_ANY) void fourMonths_AreEndingWithBer (Month month) {EnumSet months = EnumSet.of (Month.SEPTEMBER, Month.OC. .STUDENI, mjesec.DECEMBAR); assertTrue (months.contens (month)); }

Sasvim slično kao @ValueSource, @EnumSource primjenjivo je samo kada ćemo proslijediti samo jedan argument po izvršenju testa.

4.4. CSV Literals

Pretpostavimo da ćemo se pobrinuti da toUpperCase () metoda iz Niz generira očekivanu veliku veličinu. @ValueSource neće biti dovoljno.

Da bismo napisali parametarski test za takve scenarije, moramo:

  • Prođite ulazna vrijednost i an očekivana vrijednost na metodu ispitivanja
  • Izračunajte stvarni rezultat s tim ulaznim vrijednostima
  • Tvrditi stvarna vrijednost s očekivanom vrijednošću

Dakle, trebaju nam izvori argumenata koji mogu proslijediti višestruke argumente. The @CsvSource jedan je od tih izvora:

@ParameterizedTest @CsvSource ({"test, TEST", "tEst, TEST", "Java, JAVA"}) void toUpperCase_ShouldGenerateTheExpectedUppercaseValue (String input, String očekuje) {String actualValue = input.toUpperCase (); assertEquals (očekivana, stvarna vrijednost); }

The @CsvSource prihvaća niz vrijednosti odvojenih zarezom i svaki unos niza odgovara retku u CSV datoteci.

Ovaj izvor svaki put uzima jedan unos niza, dijeli ga zarezom i svaki niz prosljeđuje označenoj testnoj metodi kao zasebne parametre. Prema zadanim postavkama zarez je odvajač stupaca, ali ga možemo prilagoditi pomoću graničnik atribut:

@ParameterizedTest @CsvSource (value = {"test: test", "tEst: test", "Java: java"}, graničnik = ':') void toLowerCase_ShouldGenerateTheExpectedLowercaseValue (unos niza, očekivani niz) {String actualValueCase input (toLower) ); assertEquals (očekivana, stvarna vrijednost); }

Sada je to debelo crijevo-odvojena vrijednost, još uvijek CSV!

4.5. CSV datoteke

Umjesto prosljeđivanja CSV vrijednosti unutar koda, možemo se pozvati na stvarnu CSV datoteku.

Na primjer, mogli bismo koristiti CSV datoteku poput:

ulaz, očekivani test, TEST TEST, TEST Java, JAVA

Možemo učitati CSV datoteku i zanemari stupac zaglavlja s @CsvFileSource:

@ParameterizedTest @CsvFileSource (resources = "/data.csv", numLinesToSkip = 1) void toUpperCase_ShouldGenerateTheExpectedUppercaseValueCSVFile (String input, String očekuje) {String actualValue = input.toUpperCase (); assertEquals (očekivana, stvarna vrijednost); }

The resursi atribut predstavlja resurse CSV datoteke na stazi za čitanje. I možemo mu proslijediti više datoteka.

The numLinesToSkip atribut predstavlja broj redaka koje treba preskočiti prilikom čitanja CSV datoteka. Prema zadanim postavkama, @CsvFileSource ne preskače nijednu liniju, ali ova je značajka obično korisna za preskakanje linija zaglavlja, kao što smo to učinili ovdje.

Baš kao i jednostavno @CsvSource, graničnik je prilagodljiv s graničnik atribut.

Pored separatora stupaca:

  • Razdjelnik crta može se prilagoditi pomoću lineSeparator atribut - novi red je zadana vrijednost
  • Kodiranje datoteke je prilagodljivo pomoću kodiranje atribut - UTF-8 je zadana vrijednost

4.6. Metoda

Izvori argumenata koje smo do sada obradili pomalo su jednostavni i dijele jedno ograničenje: teško je ili nemoguće proslijediti složene objekte pomoću njih!

Jedan pristup pružanje složenijih argumenata je korištenje metode kao izvora argumenata.

Isprobajmo isBlank metoda s a @MethodSource:

@ParameterizedTest @MethodSource ("provideStringsForIsBlank") void isBlank_ShouldReturnTrueForNullOrBlankStrings (String input, boolean očekuje) {assertEquals (očekuje se, Strings.isBlank (ulaz)); }

Ime kojemu dobavljamo @MethodSource mora odgovarati postojećoj metodi.

Pa hajde da dalje napišemo provideStringsForIsBlank, a statički metoda koja vraća a Stream od Arguments:

privatni statični tok pružaStringsForIsBlank () {vraća Stream.of (Arguments.of (null, true), Arguments.of ("", true), Arguments.of ("", true), Arguments.of ("nije prazno", lažno)); }

Ovdje doslovno vraćamo tok argumenata, ali to nije strog zahtjev. Na primjer, možemo vratiti bilo koje sučelje nalik zbirci poput Popis.

Ako ćemo pružiti samo jedan argument po pozivu testa, tada nije potrebno koristiti Argumenti apstrakcija:

@ParameterizedTest @MethodSource // hmm, bez naziva metode ... void isBlank_ShouldReturnTrueForNullOrBlankStringsOneArgument (String input) {assertTrue (Strings.isBlank (input)); } privatni statički tok jeBlank_ShouldReturnTrueForNullOrBlankStringsOneArgument () {return Stream.of (null, "", ""); }

Kada ne navedemo ime za @MethodSource, JUnit će tražiti izvornu metodu s istim imenom kao i test metoda.

Ponekad je korisno dijeliti argumente između različitih klasa testa. U tim slučajevima izvornu metodu izvan trenutne klase možemo nazvati njezinim potpuno kvalificiranim imenom:

klasa StringsUnitTest {@ParameterizedTest @MethodSource ("com.baeldung.parameterized.StringParams # blankStrings") void isBlank_ShouldReturnTrueForNullOrBlankStringsExternalSource (String input) {assertTrue (input) .isBlank (input); }} javna klasa StringParams {static Stream blankStrings () {return Stream.of (null, "", ""); }}

Koristiti FQN # methodName formatu možemo se pozvati na vanjsku statičku metodu.

4.7. Davatelj prilagođenih argumenata

Sljedeći napredni pristup za prosljeđivanje testnih argumenata je uporaba prilagođene implementacije sučelja tzv ArgumentsProvider:

klasa BlankStringsArgumentsProvider implementira ArgumentsProvider {@Override javni Stream provideArguments (ExtensionContext context) {return Stream.of (Arguments.of ((String) null), Arguments.of (""), Arguments.of ("")); }}

Tada svoj test možemo označiti s @ArgumentsSource napomena za upotrebu ovog prilagođenog davatelja usluga:

@ParameterizedTest @ArgumentsSource (BlankStringsArgumentsProvider.class) void isBlank_ShouldReturnTrueForNullOrBlankStringsArgProvider (String input) {assertTrue (Strings.isBlank (input)); }

Učinimo prilagođenog davatelja ugodnijim API-jem za upotrebu s prilagođenom bilješkom!

4.8. Prilagođena bilješka

Što kažete na učitavanje testnih argumenata iz statičke varijable? Nešto kao:

statički argumenti streama = Stream.of (Arguments.of (null, true), // null nizove treba smatrati praznim Arguments.of ("", true), Arguments.of ("", true), Arguments.of (" nije prazno ", false)); @ParameterizedTest @VariableSource ("argumenti") void jeBlank_ShouldReturnTrueForNullOrBlankStringsVariableSource (unos niza, očekuje se logička vrijednost) {assertEquals (očekuje se, Strings.isBlank (ulaz)); }

Zapravo, JUnit 5 to ne pruža! Međutim, možemo pokrenuti vlastito rješenje.

Kao prvo, možemo stvoriti napomenu:

@Documented @Target (ElementType.METHOD) @Retention (RetentionPolicy.RUNTIME) @ArgumentsSource (VariableArgumentsProvider.class) public @interface VariableSource {/ ** * Ime statičke varijable * / String value (); }

Tada trebamo nekako konzumirajte napomenu pojedinosti i dati argumente za ispitivanje. JUnit 5 pruža dvije apstrakcije za postizanje te dvije stvari:

  • AnnotationConsumer potrošiti detalje bilješke
  • ArgumentsProvider pružiti testne argumente

Dakle, dalje trebamo napraviti VariableArgumentsProvider klasa čita iz navedene statičke varijable i vraća njezinu vrijednost kao testne argumente:

class VariableArgumentsProvider implementira ArgumentsProvider, AnnotationConsumer {private String variableName; @Override public Stream provideArguments (ExtensionContext context) {return context.getTestClass () .map (this :: getField) .map (this :: getValue) .orElseThrow (() -> new IllegalArgumentException ("Neuspješno učitavanje testnih argumenata") ); } @Override public void accept (VariableSource variableSource) {variableName = variableSource.value (); } privatno polje getField (Class clazz) {try {return clazz.getDeclaredField (variableName); } catch (Iznimka e) {return null; }} @SuppressWarnings ("neoznačeno") private Stream getValue (polje polja) {Vrijednost objekta = null; probajte {value = field.get (null); } catch (Izuzetak se zanemaruje) {} return value == null? null: (Stream) vrijednost; }}

I djeluje poput šarma!

5. Pretvorba argumenata

5.1. Implicitna konverzija

Napišimo ponovno jedan od tih @EnumTests s @CsvSource:

@ParameterizedTest @CsvSource ({"APRIL", "JUN", "SEPTEMBER", "NOVEMBER"}) // Pssing stringovi void someMonths_Are30DaysLongCsv (mjesec u mjesecu) {final boolean isALeapYear = false; assertEquals (30, month.length (isALeapYear)); }

Ovo ne bi trebalo raditi, zar ne? Ali, nekako je tako!

Dakle, JUnit 5 pretvara Niz argumenti navedenom tipu nabrajanja. Kako bi podržao slučajeve korištenja poput ovog, JUnit Jupiter nudi niz ugrađenih pretvarača implicitnog tipa.

Postupak pretvorbe ovisi o deklariranoj vrsti svakog parametra metode. Implicitna pretvorba može pretvoriti Niz instance na vrste poput:

  • UUID
  • Lokalno
  • LocalDate, LocalTime, LocalDateTime, godina, mjesec itd.
  • Datoteka i Staza
  • URL i URI
  • Enum potklase

5.2. Eksplicitna konverzija

Ponekad moramo pružiti prilagođeni i eksplicitni pretvarač za argumente.

Pretpostavimo da želimo pretvoriti nizove s gggg / mm / ddformatirati u LocalDate instance. Prvo, moramo implementirati ArgumentConverter sučelje:

klasa SlashyDateConverter implementira ArgumentConverter {@Override javni pretvorba objekta (izvor objekta, kontekst ParameterContext) baca ArgumentConversionException {if (! (source instanceof String)) {throw new IllegalArgumentException ("Argument treba biti niz:" + izvor); } isprobajte {String [] parts = ((String) source) .split ("/"); int godina = Integer.parseInt (dijelovi [0]); int mjesec = Integer.parseInt (dijelovi [1]); int dan = Integer.parseInt (dijelovi [2]); return LocalDate.of (godina, mjesec, dan); } catch (Iznimka e) {bacanje novog IllegalArgumentException ("Nije uspjelo pretvoriti", e); }}}

Tada bismo se trebali obratiti pretvaraču putem @ConvertWith napomena:

@ParameterizedTest @CsvSource ({"2018/12 / 25,2018", "2019/02 / 11,2019"}) void getYear_ShouldWorkAsExpected (@ConvertWith (SlashyDateConverter.class) Datum lokalnog datuma, očekuje se) {assertEquals (očekuje se, datum. getYear ()); }

6. Argument Accessor

Prema zadanim postavkama, svaki argument koji se daje parametarskom testu odgovara jednom parametru metode. Slijedom toga, kada prosljeđujete pregršt argumenata putem izvora argumenata, potpis metode ispitivanja postaje vrlo velik i neuredan.

Jedan od pristupa rješavanju ovog problema je inkapsuliranje svih proslijeđenih argumenata u instancu ArgumentsAccessor i dohvatiti argumente prema indeksu i tipu.

Na primjer, razmotrimo naš Osoba razred:

razred Osoba {String firstName; Niz srednje ime; String lastName; // konstruktor public String fullName () {if (middleName == null || middleName.trim (). isEmpty ()) {return String.format ("% s% s", firstName, lastName); } return String.format ("% s% s% s", ime, ime, prezime); }}

Zatim, kako bi se testirao puno ime() metodom, proslijedit ćemo četiri argumenta: ime Srednje ime prezime, i očekuje se puno ime. Možemo koristiti ArgumentsAccessor za dohvaćanje testnih argumenata umjesto da ih deklariraju kao parametre metode:

@ParameterizedTest @CsvSource ({"Isaac ,, Newton, Isaac Newton", "Charles, Robert, Darwin, Charles Robert Darwin"}) void fullName_ShouldGenerateTheExpectedFullName (ArgumentsAccessor argumentsAccessor) {String firstName = argumentsAccessor.getString; Niz srednje ime = (niz) argumentAccessor.get (1); Niz lastName = argumentsAccessor.get (2, String.class); Niz očekivanoFullName = argumentsAccessor.getString (3); Osoba osoba = nova Osoba (ime, ime, prezime); assertEquals (očekivanoFullName, person.fullName ()); }

Ovdje enkapsuliramo sve proslijeđene argumente u ArgumentsAccessor instance, a zatim u tijelu metode ispitivanja dohvaćanje svakog prosljeđenog argumenta sa svojim indeksom. Osim što je samo pristupnik, pretvorba tipa podržana je i putem dobiti* metode:

  • getString (indeks) dohvaća element u određenom indeksu i pretvara ga u Niztisto vrijedi i za primitivne tipove
  • dobiti (indeks) jednostavno dohvaća element s određenim indeksom kao Objekt
  • dobiti (indeks, vrsta) dohvaća element u određenom indeksu i pretvara ga u zadani tip

7. Agregator argumenata

Koristiti ArgumentsAccessor apstrakcija izravno može učiniti testni kod manje čitljivim ili ponovnim. Kako bismo riješili ove probleme, možemo napisati prilagođeni i višekratni agregator.

Da bismo to učinili, provodimo ArgumentiAgregator sučelje:

klasa PersonAggregator implementira ArgumentsAggregator {@Override public Object aggregateArguments (ArgumentsAccessor accessor, ParameterContext context) baca ArgumentsAggregationException {return new Person (accessor.getString (1), accessor.getString (2), accessor.get.get. }}

A onda ga referenciramo putem @AggregateWith napomena:

@ParameterizedTest @CsvSource ({"Isaac Newton, Isaac ,, Newton", "Charles Robert Darwin, Charles, Robert, Darwin"}) void fullName_ShouldGenerateTheExpectedFullName (String ожиdeFullName, @AggregateWith (PersonAggregator.class) PersonName (PersonAggregator.class) Person person.fullName ()); }

The Agregator osoba uzima posljednja tri argumenta i instancira a Osoba razred iz njih.

8. Prilagođavanje prikazanih imena

Prema zadanim postavkama, ime za prikaz parametarskog testa sadrži indeks pozivanja zajedno s Niz prikaz svih proslijeđenih argumenata, otprilike poput:

├─ someMonths_Are30DaysLongCsv (Month) │ │ ├─ [1] TRAVANJ │ │ ├─ [2] LIPANJ │ │ ├─ [3] SEPTEMBAR │ │ └─ [4] STUDENI

Međutim, ovaj zaslon možemo prilagoditi putem Ime atribut @ParameterizedTest napomena:

@ParameterizedTest (name = "{index} {0} traje 30 dana") @EnumSource (value = Month.class, names = {"APRIL", "JUN", "SEPTEMBER", "NOVEMBER"}) void someMonths_Are30DaysLong ( Mjesec mjesec) {final boolean isALeapYear = false; assertEquals (30, month.length (isALeapYear)); }

Travanj je dug 30 dana zasigurno je čitljivije ime za prikaz:

├─ someMonths_Are30DaysLong (Month) │ │ ├─ 1. TRAVNJA traje 30 dana │ │ ├─ 2. LIPNJA traje 30 dana │ │ ├─ 3. RUJNA traje 30 dana │ │ └─ 4. NOVEMBRA traje 30 dana

Sljedeća rezervirana mjesta dostupna su prilikom prilagođavanja imena za prikaz:

  • {indeks} bit će zamijenjen indeksom prizivanja - jednostavno rečeno, indeks prizivanja za prvo izvršenje je 1, za drugo 2 i tako dalje
  • {argumenti} je rezervirano mjesto za cjeloviti popis argumenata odvojenih zarezom
  • {0}, {1}, ... su rezervirana mjesta za pojedinačne argumente

9. Zaključak

U ovom smo članku istražili matice i vijke parametarskih ispitivanja u JUnit 5.

Saznali smo da se parametarski testovi razlikuju od uobičajenih testova u dva aspekta: označeni su s @ParameterizedTest, i trebaju im izvor za svoje deklarirane argumente.

Također, do sada bismo trebali da JUnit nudi neke mogućnosti za pretvaranje argumenata u prilagođene vrste ciljeva ili za prilagođavanje naziva testova.

Kao i obično, uzorci kodova dostupni su na našem GitHub projektu, zato ga provjerite!