Veiledning til JUnit 5 parametrerte tester

1. Oversikt

JUnit 5, neste generasjon av JUnit, letter skriving av utviklertester med nye og skinnende funksjoner.

En slik funksjon er sarameteriserte tester. Denne funksjonen gjør det mulig for oss å utføre en enkelt testmetode flere ganger med forskjellige parametere.

I denne veiledningen skal vi utforske parametriserte tester i dybden, så la oss komme i gang!

2. Avhengigheter

For å kunne bruke JUnit 5-parametrerte tester, må vi importere junit-jupiter-params gjenstand fra JUnit Platform. Det betyr at når du bruker Maven, vil vi legge til følgende i vår pom.xml:

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

Når vi bruker Gradle, spesifiserer vi det også litt annerledes:

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

3. Førsteinntrykk

La oss si at vi har en eksisterende verktøyfunksjon, og vi vil være trygg på oppførselen:

public class Numbers {public static boolean isOdd (int number) {return number% 2! = 0; }}

Parameteriserte tester er som andre tester bortsett fra at vi legger til @ParameterizedTest kommentar:

@ParameterizedTest @ValueSource (ints = {1, 3, 5, -3, 15, Integer.MAX_VALUE}) // seks tall ugyldige erOdd_ShouldReturnTrueForOddNumbers (int number) {assertTrue (Numbers.isOdd (number)); }

JUnit 5 testløper utfører testen ovenfor - og følgelig erOdd metode - seks ganger. Og hver gang tildeler den en annen verdi enn @ValueSource array til Nummer metodeparameter.

Så dette eksemplet viser oss to ting vi trenger for en parameterisert test:

  • en kilde til argumenter, en int array, i dette tilfellet
  • en måte å få tilgang til dem, i dette tilfellet, Nummer parameter

Det er også en ting som ikke er tydelig med dette eksemplet, så følg med.

4. Argumentkilder

Som vi burde vite nå, utfører en parameterisert test den samme testen flere ganger med forskjellige argumenter.

Og forhåpentligvis kan vi gjøre mer enn bare tall - så la oss utforske!

4.1. Enkle verdier

Med @ValueSource kommentar, vi kan overføre en rekke bokstavelige verdier til testmetoden.

Anta for eksempel at vi skal teste det enkle er tom metode:

public class Strings {public static boolean isBlank (String input) return input == null}

Vi forventer at denne metoden kommer tilbake ekte til null for blanke strenger. Så vi kan skrive en parameterisert test som følgende for å hevde denne oppførselen:

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

Som vi kan se, vil JUnit kjøre denne testen to ganger, og tildeler hver gang ett argument fra matrisen til metodeparameteren.

En av begrensningene for verdikilder er at de bare støtter følgende typer:

  • kort (med shorts Egenskap)
  • byte (med byte Egenskap)
  • int (med ints Egenskap)
  • lang (med lengter Egenskap)
  • flyte (med flyter Egenskap)
  • dobbelt (med dobler Egenskap)
  • røye (med tegn Egenskap)
  • java.lang.Streng (med strenger Egenskap)
  • java.lang.Klasse (med klasser Egenskap)

Også, vi kan bare sende ett argument til testmetoden hver gang.

Og før noen gikk videre, la noen merke til at vi ikke passerte null som argument? Det er en annen begrensning: Vi kan ikke passere null gjennom en @ValueSource, selv for String og Klasse!

4.2. Null og tomme verdier

Fra og med JUnit 5.4 kan vi passere en singel null verdi til en parameterisert testmetode ved hjelp av @NullSource:

@ParameterizedTest @NullSource ugyldig isBlank_ShouldReturnTrueForNullInputs (String input) {assertTrue (Strings.isBlank (input)); }

Siden primitive datatyper ikke kan akseptere null verdier, kan vi ikke bruke @NullSource for primitive argumenter.

Ganske lignende kan vi sende tomme verdier ved hjelp av @EmptySource kommentar:

@ParameterizedTest @EmptySource ugyldig isBlank_ShouldReturnTrueForEmptyStrings (strenginngang) {assertTrue (Strings.isBlank (input)); }

@EmptySource sender et enkelt tomt argument til den merkede metoden.

Til String argumenter, ville den passerte verdien være så enkel som en tom String. Videre kan denne parameterkilden gi tomme verdier for Samling typer og matriser.

For å passere begge deler null og tomme verdier, kan vi bruke det sammensatte @NullAndEmptySource kommentar:

@ParameterizedTest @NullAndEmptySource ugyldig erBlank_ShouldReturnTrueForNullAndEmptyStrings (strenginngang) {assertTrue (Strings.isBlank (input)); }

Som med @EmptySource, den komponerte kommentaren fungerer for Strings,Samlings, og matriser.

For å overføre noen flere tomme strengvarianter til den parametriserte testen, vi kan kombinere @ValueSource, @NullSource og @EmptySource sammen:

@ParameterizedTest @NullAndEmptySource @ValueSource (strings = {"", "\ t", "\ n"}) ugyldig erBlank_ShouldReturnTrueForAllTypesOfBlankStrings (Strenginngang) {assertTrue (Strings.isBlank (input)) }

4.3. Enum

For å kjøre en test med forskjellige verdier fra en oppregning, kan vi bruke @EnumSource kommentar.

For eksempel kan vi hevde at alle månedstallene er mellom 1 og 12:

@ParameterizedTest @EnumSource (Month.class) // passerer alle 12 månedene ugyldig getValueForAMonth_IsAlwaysBetweenOneAndTwelve (Månedmåned) {int monthNumber = month.getValue (); assertTrue (monthNumber> = 1 && monthNumber <= 12); }

Eller vi kan filtrere ut noen måneder ved å bruke navn Egenskap.

Hva med å hevde at april, september, juni og november er 30 dager:

@ParameterizedTest @EnumSource (verdi = Month.class, names = {"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER"}) ugyldig someMonths_Are30DaysLong (Månedmåned) {final boolean isALeapYear = false; assertEquals (30, month.length (isALeapYear)); }

Som standard er navn vil bare beholde de matchede enumverdiene. Vi kan snu dette ved å stille inn modus tilskrive UTELUKKE:

@ParameterizedTest @EnumSource (verdi = Month.class, names = {"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER", "FEBRUARY"}, mode = EnumSource.Mode.EXCLUDE) ugyldig unntatt FourMonths_OthersAre31DaysLong (Måned måned) siste boolske isALeapYear = false; assertEquals (31, month.length (isALeapYear)); }

I tillegg til bokstavelige strenger, kan vi gi et vanlig uttrykk til navn Egenskap:

@ParameterizedTest @EnumSource (verdi = Måned.klasse, navn = ". + BER", modus = EnumSource.Mode.MATCH_ANY) ugyldig fourMonths_AreEndingWithBer (Månedmåned) {EnumSet måneder = EnumSet.of (Måned.SEPTEMBER, Måned.OCTOBER, Måned .NOVEMBER, måned.DESEMBER); assertTrue (måneder. inneholder (måned)); }

Ganske lik @ValueSource, @EnumSource gjelder bare når vi skal sende bare ett argument per testutførelse.

4.4. CSV-bokstaver

Anta at vi skal sørge for at toUpperCase () metode fra String genererer forventet store bokstaver. @ValueSource vil ikke være nok.

For å skrive en parameterisert test for slike scenarier, må vi:

  • Bestå en inngangsverdi og en forventet verdi til testmetoden
  • Beregn faktisk resultat med disse inngangsverdiene
  • Påstå den faktiske verdien med forventet verdi

Så vi trenger argumentkilder som kan sende flere argumenter. De @CsvSource er en av disse kildene:

@ParameterizedTest @CsvSource ({"test, TEST", "tEst, TEST", "Java, JAVA"}) ugyldig toUpperCase_ShouldGenerateTheExpectedUppercaseValue (strenginngang, streng forventet) {String actualValue = input.toUpperCase (); assertEquals (forventet, actualValue); }

De @CsvSource aksepterer en matrise med kommaadskilte verdier, og hver matrisepost tilsvarer en linje i en CSV-fil.

Denne kilden tar en matrisepost hver gang, deler den med komma og sender hver matrise til den merkede testmetoden som separate parametere. Som standard er kommaet kolonneutskilleren, men vi kan tilpasse det ved hjelp av avgrensning Egenskap:

@ParameterizedTest @CsvSource (verdi = {"test: test", "tEst: test", "Java: java"}, delimiter = ':') ugyldig tilLowerCase_ShouldGenerateTheExpectedLowercaseValue (strenginngang, streng forventet) {String actualValue = input.toLowerCase ( ); assertEquals (forventet, actualValue); }

Nå er det en kolon-skilt verdi, fremdeles en CSV!

4.5. CSV-filer

I stedet for å sende CSV-verdiene inne i koden, kan vi referere til en faktisk CSV-fil.

For eksempel kan vi bruke en CSV-fil som:

input, forventet test, TEST tEst, TEST Java, JAVA

Vi kan laste inn CSV-filen og ignorere overskriftskolonnen med @CsvFileSource:

@ParameterizedTest @CsvFileSource (resources = "/data.csv", numLinesToSkip = 1) ugyldig toUpperCase_ShouldGenerateTheExpectedUppercaseValueCSVFile (strenginngang, streng forventet) {String actualValue = input.toUpperCase (); assertEquals (forventet, actualValue); }

De ressurser attributt representerer CSV-filressursene på klassestien som skal leses. Og vi kan sende flere filer til den.

De numLinesToSkip attributt representerer antall linjer å hoppe over når du leser CSV-filene. Som standard, @CsvFileSource hopper ikke over noen linjer, men denne funksjonen er vanligvis nyttig for å hoppe over topplinjene, som vi gjorde her.

Akkurat som det enkle @CsvSource, avgrenseren kan tilpasses med avgrensning Egenskap.

I tillegg til kolonneskilleren:

  • Linjeseparatoren kan tilpasses ved hjelp av lineSeparator attributt - en ny linje er standardverdien
  • Filkodingen kan tilpasses ved hjelp av koding attributt - UTF-8 er standardverdien

4.6. Metode

Argumentkildene vi har dekket så langt er noe enkle og deler en begrensning: Det er vanskelig eller umulig å passere komplekse objekter ved å bruke dem!

En tilnærming til å gi mer komplekse argumenter er å bruke en metode som argumentkilde.

La oss teste er tom metode med en @MethodSource:

@ParameterizedTest @MethodSource ("supplyStringsForIsBlank") ugyldig erBlank_ShouldReturnTrueForNullOrBlankStrings (strenginngang, forventet boolsk) {assertEquals (forventet, Strings.isBlank (input)) }

Navnet vi leverer til @MethodSource må matche en eksisterende metode.

Så la oss skrive neste gang supplyStringsForIsBlank, en statisk metode som returnerer a Strøm av Arguments:

privat statisk strøm supplyStringsForIsBlank () {return Stream.of (Arguments.of (null, true), Arguments.of ("", true), Arguments.of ("", true), Arguments.of ("not blank", falsk) ); }

Her returnerer vi bokstavelig talt en strøm av argumenter, men det er ikke et strengt krav. For eksempel, vi kan returnere andre samlingslignende grensesnitt som Liste.

Hvis vi skal gi bare ett argument per testinnkalling, er det ikke nødvendig å bruke Argumenter abstraksjon:

@ParameterizedTest @MethodSource // hmm, no method name ... void isBlank_ShouldReturnTrueForNullOrBlankStringsOneArgument (Streng input) {assertTrue (Strings.isBlank (input)); } privat statisk strøm erBlank_ShouldReturnTrueForNullOrBlankStringsOneArgument () {return Stream.of (null, "", ""); }

Når vi ikke gir et navn på @MethodSource, JUnit vil søke etter en kildemetode med samme navn som testmetoden.

Noen ganger er det nyttig å dele argumenter mellom forskjellige testklasser. I disse tilfellene kan vi referere til en kildemetode utenfor gjeldende klasse ved å være fullt kvalifisert navn:

class StringsUnitTest {@ParameterizedTest @MethodSource ("com.baeldung.parameterized.StringParams # blankStrings") void isBlank_ShouldReturnTrueForNullOrBlankStringsExternalSource (Streng input) {assertTrue (Strings.isBlank (input)) }} offentlig klasse StringParams {statisk Stream blankStrings () {return Stream.of (null, "", ""); }}

Bruker FQN # methodName format kan vi referere til en ekstern statisk metode.

4.7. Tilpasset argumentleverandør

En annen avansert tilnærming for å bestå testargumenter er å bruke en tilpasset implementering av et grensesnitt som heter Argumenter Leverandør:

klasse BlankStringsArgumentsProvider implementerer ArgumentsProvider {@Override public Stream supplyArguments (ExtensionContext context) {return Stream.of (Arguments.of ((String) null), Arguments.of (""), Arguments.of ("")); }}

Så kan vi kommentere testen vår med @ArgumentsSource kommentar for å bruke denne egendefinerte leverandøren:

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

La oss gjøre den tilpassede leverandøren til en mer behagelig API å bruke med en tilpasset kommentar!

4.8. Egendefinert kommentar

Hva med å laste testargumentene fra en statisk variabel? Noe som:

statiske strømargumenter = Strøm.of (Argumenter.of (null, sant), // nullstrenger skal betraktes som tomme Argumenter.of ("", true), Arguments.of ("", true), Arguments.of (" ikke blank ", false)); @ParameterizedTest @VariableSource ("argumenter") ugyldig erBlank_ShouldReturnTrueForNullOrBlankStringsVariableSource (strenginngang, boolsk forventet) {assertEquals (forventet, Strings.isBlank (input)); }

Faktisk, JUnit 5 gir ikke dette! Imidlertid kan vi rulle vår egen løsning.

For det første kan vi lage en kommentar:

@Documented @Target (ElementType.METHOD) @Retention (RetentionPolicy.RUNTIME) @ArgumentsSource (VariableArgumentsProvider.class) public @interface VariableSource {/ ** * Navnet på den statiske variabelen * / strengverdi (); }

Da må vi på en eller annen måte konsumere kommentaren detaljer og gi testargumenter. JUnit 5 gir to abstraksjoner for å oppnå disse to tingene:

  • Kommentar Forbruker for å konsumere merknadsdetaljene
  • Argumenter - leverandør å gi testargumenter

Så, vi må neste gjøre VariableArgumentsProvider klasse lest fra den angitte statiske variabelen og returnerer verdien som testargumenter:

klasse VariableArgumentsProvider implementerer ArgumentsProvider, AnnotationConsumer {private String variableName; @Override public Stream supplyArguments (ExtensionContext context) {return context.getTestClass () .map (this :: getField) .map (this :: getValue) .orElseThrow (() -> new IllegalArgumentException ("Kunne ikke laste testargumenter") ); } @ Override public void accept (VariableSource variableSource) {variableName = variableSource.value (); } privat felt getField (Class clazz) {prøv {return clazz.getDeclaredField (variableName); } fange (Unntak e) {return null; }} @SuppressWarnings ("ukontrollert") privat Stream getValue (feltfelt) {Objektverdi = null; prøv {value = field.get (null); } fange (Unntak ignorert) {} returverdi == null? null: (Stream) verdi; }}

Og det fungerer som en sjarm!

5. Argumentkonvertering

5.1. Implisitt konvertering

La oss omskrive en av dem @EnumTests med en @CsvKilde:

@ParameterizedTest @CsvSource ({"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER"}) // Pssing strings void someMonths_Are30DaysLongCsv (Month month) {final boolean isALeapYear = false; assertEquals (30, month.length (isALeapYear)); }

Dette burde ikke fungere, ikke sant? Men, på en eller annen måte gjør det det!

Så, JUnit 5 konverterer String argumenter til den spesifiserte enumtypen. For å støtte brukstilfeller som dette, tilbyr JUnit Jupiter et antall innebygde implisitte type omformere.

Konverteringsprosessen avhenger av den deklarerte typen for hver metodeparameter. Den implisitte konverteringen kan konvertere String forekomster til typer som:

  • UUID
  • Lokal
  • LocalDate, LocalTime, LocalDateTime, Year, Month, etc.
  • Fil og Sti
  • URL og URI
  • Enum underklasser

5.2. Eksplisitt konvertering

Noen ganger må vi gi en tilpasset og eksplisitt omformer for argumenter.

Anta at vi vil konvertere strenger med åååå / mm / ddformat til LocalDate tilfeller. Først må vi implementere ArgumentConverter grensesnitt:

klasse SlashyDateConverter implementerer ArgumentConverter {@Override public Object convert (Object source, ParameterContext context) kaster ArgumentConversionException {if (! (source instanceof String)) {throw new IllegalArgumentException ("Argumentet skal være en streng:" + kilde); } prøv {String [] parts = ((String) source) .split ("/"); int år = Integer.parseInt (deler [0]); int måned = Integer.parseInt (deler [1]); int day = Integer.parseInt (parts [2]); returner LocalDate.of (år, måned, dag); } fange (Unntak e) {kast ny IllegalArgumentException ("Kunne ikke konvertere", e); }}}

Deretter bør vi referere til omformeren via @ConvertWith kommentar:

@ParameterizedTest @CsvSource ({"2018/12 / 25,2018", "2019/02 / 11,2019"}) ugyldig getYear_ShouldWorkAsExpected (@ConvertWith (SlashyDateConverter.class) LocalDate date, int forventet) {assertEquals (forventet, dato. getYear ()); }

6. Argument Accessor

Som standard tilsvarer hvert argument som leveres til en parameterisert test en parameter for en enkelt metode. Når man sender en håndfull argumenter via en argumentkilde, blir testmetodesignaturen veldig stor og rotete.

En tilnærming for å løse dette problemet er å kapsle inn alle argumentene som er sendt inn i en forekomst av ArgumenterAccessor og hente argumenter etter indeks og type.

La oss for eksempel vurdere våre Person klasse:

klasse Person {String fornavn; Streng mellomnavn; Strengens etternavn; // constructor public String fullName () {if (middleName == null || middleName.trim (). isEmpty ()) {return String.format ("% s% s", fornavn, etternavn); } returner String.format ("% s% s% s", fornavn, mellomnavn, etternavn); }}

For å teste fullt navn() metode, vil vi sende fire argumenter: fornavn mellomnavn etternavn, og forventet fullnavn. Vi kan bruke ArgumenterAccessor for å hente testargumentene i stedet for å erklære dem som metodeparametere:

@ParameterizedTest @CsvSource ({"Isaac ,, Newton, Isaac Newton", "Charles, Robert, Darwin, Charles Robert Darwin"}) ugyldig fullName_ShouldGenerateTheExpectedFullName (ArgumentsAccessor argumenterAccessor) {String firstName = argumenterAccessor.getString (0); Streng mellomnavn = (Streng) argumenterAccessor.get (1); Streng etternavn = argumentAccessor.get (2, String.class); Streng expectFullName = argumenterAccessor.getString (3); Personperson = ny person (fornavn, mellomnavn, etternavn); assertEquals (expectFullName, person.fullName ()); }

Her innkapsler vi alle overførte argumenter i et ArgumenterAccessor forekomst og deretter, i testmetoden, henter hvert bestått argument med indeksen. I tillegg til å bare være tilbehør, støttes typekonvertering gjennom få* metoder:

  • getString (indeks) henter et element i en bestemt indeks og konverterer det til Stringtdet samme gjelder for primitive typer
  • få (indeks) bare henter et element i en bestemt indeks som en Gjenstand
  • få (indeks, type) henter et element i en bestemt indeks og konverterer det til det gitte type

7. Argumentaggregator

Bruker ArgumenterAccessor abstraksjon direkte kan gjøre testkoden mindre lesbar eller gjenbrukbar. For å løse disse problemene kan vi skrive en tilpasset og gjenbrukbar aggregator.

For å gjøre det implementerer vi Argumenter Aggregator grensesnitt:

klasse PersonAggregator implementerer ArgumentsAggregator {@Override public Object aggregateArguments (ArgumentsAccessor accessor, ParameterContext context) kaster ArgumentsAggregationException {return new Person (accessor.getString (1), accessor.getString (2), accessor.getString (3)); }}

Og så refererer vi til det via @AggregateWith kommentar:

@ParameterizedTest @CsvSource ({"Isaac Newton, Isaac ,, Newton", "Charles Robert Darwin, Charles, Robert, Darwin"}) ugyldig fullName_ShouldGenerateTheExpectedFullName (String expectFullName, @AggregateWith (PersonAggregator.class) Person person) {assertEquals (expectFullName person.fullnavn ()); }

De PersonAggregator tar de siste tre argumentene og instantierer a Person klasse ut av dem.

8. Tilpasse visningsnavn

Som standard inneholder visningsnavnet for en parameterisert test en påkallingsindeks sammen med en String representasjon av alle godkjente argumenter, noe som:

├─ someMonths_Are30DaysLongCsv (Måned) │ │ ├─ [1] APRIL │ │ ├─ [2] JUNI │ │ ├─ [3] SEPTEMBER │ │ └─ [4] NOVEMBER

Imidlertid kan vi tilpasse denne skjermen via Navn attributt til @ParameterizedTest kommentar:

@ParameterizedTest (name = "{index} {0} er 30 dager lang") @EnumSource (value = Month.class, names = {"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER"}) ugyldig someMonths_Are30DaysLong ( Måned) {final boolean isALeapYear = false; assertEquals (30, month.length (isALeapYear)); }

April er 30 dager lang er sikkert et mer lesbart visningsnavn:

├─ someMonths_Are30DaysLong (Month) │ │ ├─ 1 APRIL er 30 dager lang │ │ ├─ 2 JUNI er 30 dager lang │ │ ├─ 3 SEPTEMBER er 30 dager lang │ │ └─ 4 NOVEMBER er 30 dager lang

Følgende plassholdere er tilgjengelige når du tilpasser visningsnavnet:

  • {indeks} vil bli erstattet med påkallingsindeksen - enkelt sagt, påkallingsindeksen for første utførelse er 1, for den andre er 2, og så videre
  • {argumenter} er en plassholder for den komplette, kommaseparerte listen over argumenter
  • {0}, {1}, ... er plassholdere for individuelle argumenter

9. Konklusjon

I denne artikkelen har vi utforsket muttere og bolter til parametriserte tester i JUnit 5.

Vi lærte at parametriserte tester er forskjellige fra normale tester i to aspekter: de er merket med @ParameterizedTest, og de trenger en kilde for sine erklærte argumenter.

Nå skal vi nå at JUnit gir noen fasiliteter for å konvertere argumentene til egendefinerte måltyper eller tilpasse testnavnene.

Eksempelkodene er som vanlig tilgjengelige på GitHub-prosjektet vårt, så sørg for å sjekke det ut!


$config[zx-auto] not found$config[zx-overlay] not found