Vejledning til JUnit 5 parametriserede tests

1. Oversigt

JUnit 5, den næste generation af JUnit, letter skrivning af udviklertests med nye og skinnende funktioner.

En sådan funktion er sarameteriserede tests. Denne funktion gør det muligt for os at udføre en enkelt testmetode flere gange med forskellige parametre.

I denne vejledning skal vi udforske parametriserede test i dybden, så lad os komme i gang!

2. Afhængigheder

For at kunne bruge JUnit 5-parametrerede tests er vi nødt til at importere junit-jupiter-params artefakt fra JUnit Platform. Det betyder, at når du bruger Maven, tilføjer vi følgende til vores pom.xml:

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

Også, når du bruger Gradle, specificerer vi det lidt anderledes:

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

3. Første indtryk

Lad os sige, at vi har en eksisterende hjælpefunktion, og vi vil gerne være sikre på dens opførsel:

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

Parameteriserede tests er som andre tests, bortset fra at vi tilføjer @ParameterizedTest kommentar:

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

JUnit 5 testløber udfører denne ovenstående test - og følgelig erOdd metode - seks gange. Og hver gang tildeler den en anden værdi end @ValueSource array til nummer metode parameter.

Så dette eksempel viser os to ting, vi har brug for til en parametreret test:

  • en kilde til argumenter, en int array, i dette tilfælde
  • en måde at få adgang til dem, i dette tilfælde, nummer parameter

Der er også en ting mere, der ikke er tydeligt med dette eksempel, så hold dig opdateret.

4. Argumentkilder

Som vi burde vide nu, udfører en parametreret test den samme test flere gange med forskellige argumenter.

Og forhåbentlig kan vi gøre mere end bare tal - så lad os udforske det!

4.1. Enkle værdier

Med @ValueSource bemærkning, vi kan videregive en række bogstavelige værdier til testmetoden.

Antag for eksempel, at vi skal teste vores enkle er tom metode:

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

Vi forventer af denne metode at vende tilbage rigtigt til nul til tomme strenge. Så vi kan skrive en parametreret test som følgende for at hævde denne adfærd:

@ParameterizedTest @ValueSource (strings = {"", ""}) ugyldigt erBlank_ShouldReturnTrueForNullOrBlankStrings (strengindgang) {assertTrue (Strings.isBlank (input)); } 

Som vi kan se, kører JUnit denne test to gange, og tildeler hver gang et argument fra arrayet til metodeparameteren.

En af begrænsningerne ved værdikilder er, at de kun understøtter følgende typer:

  • kort (med shorts attribut)
  • byte (med bytes attribut)
  • int (med ints attribut)
  • lang (med længes attribut)
  • flyde (med flyder attribut)
  • dobbelt (med fordobler attribut)
  • char (med tegn attribut)
  • java.lang.Streng (med strenge attribut)
  • java.lang.Klasse (med klasser attribut)

Også, vi kan kun videregive ét argument til testmetoden hver gang.

Og før nogen gik videre, bemærkede nogen, at vi ikke passerede nul som et argument? Det er en anden begrænsning: Vi kan ikke passere nul gennem en @ValueSource, selv for Snor og Klasse!

4.2. Nul og tomme værdier

Fra og med JUnit 5.4 kan vi videregive en enkelt nul værdi til en parametreret testmetode ved hjælp af @NullSource:

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

Da primitive datatyper ikke kan accepteres nul værdier, kan vi ikke bruge @NullSource for primitive argumenter.

På samme måde kan vi videregive tomme værdier ved hjælp af @EmptySource kommentar:

@ParameterizedTest @EmptySource ugyldigt isBlank_ShouldReturnTrueForEmptyStrings (String input) {assertTrue (Strings.isBlank (input)); }

@EmptySource sender et enkelt tomt argument til den kommenterede metode.

Til Snor argumenter, ville den beståede værdi være så enkel som en tom Snor. Desuden kan denne parameterkilde give tomme værdier for Kollektion typer og arrays.

For at bestå begge dele nul og tomme værdier, kan vi bruge det sammensatte @NullAndEmptySource kommentar:

@ParameterizedTest @NullAndEmptySource ugyldigt erBlank_ShouldReturnTrueForNullAndEmptyStrings (String input) {assertTrue (Strings.isBlank (input)); }

Som med @EmptySource, den sammensatte kommentar fungerer for Snors,Kollektions, og arrays.

For at overføre et par flere tomme strengvarianter til den parametriserede test, vi kan kombinere @ValueSource, @NullSource og @EmptySource sammen:

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

4.3. Enum

For at køre en test med forskellige værdier fra en optælling kan vi bruge @EnumSource kommentar.

For eksempel kan vi hævde, at alle månedstal er mellem 1 og 12:

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

Eller vi kan filtrere et par måneder ud ved hjælp af navne attribut.

Hvad med at hævde, at april, september, juni og november er 30 dage lange:

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

Som standard er navne beholder kun de matchede enumværdier. Vi kan vende dette ved at indstille mode attribut til UDELUKKE:

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

Ud over bogstavelige strenge kan vi give et regelmæssigt udtryk til navne attribut:

@ParameterizedTest @EnumSource (værdi = Month.class, names = ". + BER", mode = EnumSource.Mode.MATCH_ANY) ugyldig fourMonths_AreEndingWithBer (Måned måned) {EnumSet måneder = EnumSet.of (Måned.SEPTEMBER, Måned.OCTOBER, Måned .NOVEMBER, måned.DECEMBER); assertTrue (måneder. indeholder (måned)); }

Helt ligner @ValueSource, @EnumSource kan kun anvendes, når vi kun sender et argument pr. testudførelse.

4.4. CSV-litteratur

Antag, at vi skal sørge for, at toUpperCase () metode fra Snor genererer den forventede store bogstavsværdi. @ValueSource vil ikke være nok.

For at skrive en parametreret test for sådanne scenarier skal vi:

  • Bestå en inputværdi og en forventet værdi til testmetoden
  • Beregn faktiske resultat med disse inputværdier
  • Hævde den aktuelle værdi med den forventede værdi

Så vi har brug for argumentkilder, der kan sende flere argumenter. Det @CsvSource er en af ​​disse kilder:

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

Det @CsvSource accepterer en matrix med komma-adskilte værdier, og hver matrixindgang svarer til en linje i en CSV-fil.

Denne kilde tager en matrixindgang hver gang, deler den med komma og sender hver matrix til den annoterede testmetode som separate parametre. Som standard er kommaet søjleseparatoren, men vi kan tilpasse det ved hjælp af afgrænsning attribut:

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

Nu er det en kolon-adskilt værdi, stadig en CSV!

4.5. CSV-filer

I stedet for at videregive CSV-værdierne inde i koden kan vi henvise til en faktisk CSV-fil.

For eksempel kunne vi bruge en CSV-fil som:

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

Vi kan indlæse CSV-filen og ignorere overskriftskolonnen med @CsvFileSource:

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

Det ressourcer attribut repræsenterer CSV-filressourcerne på klassestien, der skal læses. Og vi kan videregive flere filer til det.

Det numLinesToSkip attribut repræsenterer antallet af linjer, der skal springes over, når du læser CSV-filerne. Som standard, @CsvFileSource springer ikke nogen linjer over, men denne funktion er normalt nyttig til at springe over linierne over, som vi gjorde her.

Ligesom det enkle @CsvSource, skillelinjen kan tilpasses med afgrænsning attribut.

Ud over søjleseparatoren:

  • Linjeseparatoren kan tilpasses ved hjælp af lineSeparator attribut - en ny linje er standardværdien
  • Filkodningen kan tilpasses ved hjælp af indkodning attribut - UTF-8 er standardværdien

4.6. Metode

Argumentkilderne, vi hidtil har dækket, er noget enkle og deler en begrænsning: Det er svært eller umuligt at passere komplekse objekter ved hjælp af dem!

En tilgang til at levere mere komplekse argumenter er at bruge en metode som en argumentkilde.

Lad os teste er tom metode med en @MethodSource:

@ParameterizedTest @MethodSource ("giveStringsForIsBlank") ugyldig isBlank_ShouldReturnTrueForNullOrBlankStrings (String input, boolsk forventet) {assertEquals (forventet, Strings.isBlank (input)); }

Det navn, vi leverer til @MethodSource skal matche en eksisterende metode.

Så lad os derefter skrive giveStringsForIsBlank, -en statisk metode, der returnerer en Strøm af Arguments:

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

Her returnerer vi bogstaveligt talt en strøm af argumenter, men det er ikke et strengt krav. For eksempel, vi kan returnere andre samlingslignende grænseflader som f.eks Liste.

Hvis vi kun giver et argument pr. Testopkald, er det ikke nødvendigt at bruge Argumenter abstraktion:

@ParameterizedTest @MethodSource // hmm, ingen metode navn ... ugyldigt isBlank_ShouldReturnTrueForNullOrBlankStringsOneArgument (String input) {assertTrue (Strings.isBlank (input)); } privat statisk strøm erBlank_ShouldReturnTrueForNullOrBlankStringsOneArgument () {return Stream.of (null, "", ""); }

Når vi ikke giver et navn til @MethodSource, JUnit vil søge efter en kildemetode med samme navn som testmetoden.

Nogle gange er det nyttigt at dele argumenter mellem forskellige testklasser. I disse tilfælde kan vi henvise til en kildemetode uden for den aktuelle klasse ved dens fuldt kvalificerede navn:

klasse StringsUnitTest {@ParameterizedTest @MethodSource ("com.baeldung.parameterized.StringParams # blankStrings") ugyldig erBlank_ShouldReturnTrueForNullOrBlankStringsExternalSource (String input) {assertTrue (Strings.isBlank (input)) }} offentlig klasse StringParams {statisk stream blankStrings () {return Stream.of (null, "", ""); }}

Bruger FQN # methodName format kan vi henvise til en ekstern statisk metode.

4.7. Tilpasset argumentudbyder

En anden avanceret tilgang til at overføre testargumenter er at bruge en brugerdefineret implementering af en kaldet grænseflade Argumenter Udbyder:

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 vores test med @ArgumentsSource kommentar til at bruge denne brugerdefinerede udbyder:

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

Lad os gøre den brugerdefinerede udbyder til en mere behagelig API at bruge med en brugerdefineret kommentar!

4.8. Brugerdefineret kommentar

Hvad med at indlæse testargumenterne fra en statisk variabel? Noget som:

statiske streamargumenter = Stream.of (Arguments.of (null, true), // null strings skal betragtes som tomme Arguments.of ("", true), Arguments.of ("", true), Arguments.of (" ikke blank ", falsk)); @ParameterizedTest @VariableSource ("argumenter") ugyldigt erBlank_ShouldReturnTrueForNullOrBlankStringsVariableSource (strengindgang, boolsk forventet) {assertEquals (forventet, Strings.isBlank (input)); }

Rent faktisk, JUnit 5 giver ikke dette! Vi kan dog rulle vores egen løsning.

For det første kan vi oprette en kommentar:

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

Så er vi nødt til det på en eller anden måde forbruge kommentaren detaljer og give testargumenter. JUnit 5 giver to abstraktioner for at opnå disse to ting:

  • AnnotationConsumer for at forbruge annoteringsoplysningerne
  • Argumenter Udbyder at give testargumenter

Så vi skal næste gang lave VariableArgumentsProvider klasse læses fra den angivne statiske variabel og returnerer dens værdi som testargumenter:

klasse VariableArgumentsProvider implementerer ArgumentsProvider, AnnotationConsumer {private String variableName; @Override public Stream provideArguments (ExtensionContext context) {return context.getTestClass () .map (this :: getField) .map (this :: getValue) .orElseThrow (() -> new IllegalArgumentException ("Kunne ikke indlæse testargumenter") ); } @ Overstyr offentlig ugyldig accept (VariableSource variableSource) {variableName = variableSource.value (); } privat felt getField (Class clazz) {prøv {return clazz.getDeclaredField (variableName); } fange (Undtagelse e) {return null; }} @SuppressWarnings ("ikke markeret") privat Stream getValue (feltfelt) {Objektværdi = null; prøv {værdi = field.get (null); } fangst (undtagelse ignoreret) {} returværdi == null? null: (Stream) værdi; }}

Og det fungerer som en charme!

5. Argumentkonvertering

5.1. Implicit konvertering

Lad os omskrive en af ​​dem @EnumTests med en @CsvSource:

@ParameterizedTest @CsvSource ({"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER"}) // Pssingstrings ugyldige someMonths_Are30DaysLongCsv (Månedmåned) {final boolean isALeapYear = false; assertEquals (30, month.length (isALeapYear)); }

Dette burde ikke fungere, ikke? Men på en eller anden måde gør det det!

Så JUnit 5 konverterer Snor argumenter til den angivne enumtype. For at understøtte brugssager som denne leverer JUnit Jupiter et antal indbyggede implicit type konvertere.

Konverteringsprocessen afhænger af den deklarerede type for hver metodeparameter. Den implicitte konvertering kan konvertere Snor forekomster til typer som:

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

5.2. Eksplicit konvertering

Nogle gange er vi nødt til at give en brugerdefineret og eksplicit konverter til argumenter.

Antag, at vi vil konvertere strenge med åååå / mm / ddformat til LocalDate tilfælde. Først skal vi implementere ArgumentConverter grænseflade:

klasse SlashyDateConverter implementerer ArgumentConverter {@Override public Object convert (Object source, ParameterContext context) throw 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 (dele [0]); int måned = Integer.parseInt (dele [1]); int dag = Integer.parseInt (dele [2]); returnere LocalDate.of (år, måned, dag); } fange (Undtagelse e) {smid nyt IllegalArgumentException ("Kunne ikke konvertere", e); }}}

Så skal vi henvise til konverteren via @ConvertWith kommentar:

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

6. Argument Accessor

Som standard svarer hvert argument, der leveres til en parametreret test, til en enkelt metodeparameter. Når man sender en håndfuld argumenter via en argumentkilde, bliver testmetodens signatur derfor meget stor og rodet.

En tilgang til at løse dette problem er at indkapsle alle videregivne argumenter i en instans af ArgumenterAccessor og hente argumenter efter indeks og type.

Lad os for eksempel overveje vores Person klasse:

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

Derefter for at teste fulde navn() metode, vi sender fire argumenter: fornavn mellemnavn efternavn, og forventet fuldnavn. Vi kan bruge ArgumenterAccessor for at hente testargumenterne i stedet for at erklære dem som metodeparametre:

@ParameterizedTest @CsvSource ({"Isaac ,, Newton, Isaac Newton", "Charles, Robert, Darwin, Charles Robert Darwin"}) ugyldigt fullName_ShouldGenerateTheExpectedFullName (ArgumentsAccessor argumenterAccessor) {String firstName = argumenterAccessor.getString (0); String middleName = (String) argumenterAccessor.get (1); String lastName = argumenterAccessor.get (2, String.class); Streng expectFullName = argumenterAccessor.getString (3); Person person = ny person (fornavn, mellemnavn, efternavn); assertEquals (expectFullName, person.fullName ()); }

Her indkapsler vi alle overførte argumenter i et ArgumenterAccessor forekomst og derefter i testmetodekroppen at hente hvert bestået argument med dets indeks. Ud over at bare være en accessor understøttes typekonvertering igennem få* metoder:

  • getString (indeks) henter et element i et bestemt indeks og konverterer det til Snortdet samme gælder for primitive typer
  • få (indeks) simpelthen henter et element i et bestemt indeks som et Objekt
  • få (indeks, type) henter et element i et bestemt indeks og konverterer det til det givne type

7. Argumentaggregator

Bruger ArgumenterAccessor abstraktion direkte kan gøre testkoden mindre læsbar eller genanvendelig. For at løse disse problemer kan vi skrive en brugerdefineret og genanvendelig aggregator.

For at gøre det implementerer vi Argumenter Aggregator grænseflade:

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

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

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

Det PersonAggregator tager de sidste tre argumenter og instantierer a Person klasse ud af dem.

8. Tilpasning af skærmnavne

Som standard indeholder displaynavnet for en parametreret test et påkaldsindeks sammen med en Snor repræsentation af alle godkendte argumenter, noget som:

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

Vi kan dog tilpasse denne skærm via navn attribut for @ParameterizedTest kommentar:

@ParameterizedTest (name = "{index} {0} er 30 dage lang") @EnumSource (værdi = Måned.klasse, navne = {"APRIL", "JUNI", "SEPTEMBER", "NOVEMBER"}) ugyldig someMonths_Are30DaysLong ( Måned måned) {final boolean isALeapYear = false; assertEquals (30, month.length (isALeapYear)); }

April er 30 dage lang er bestemt et mere læsbart visningsnavn:

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

Følgende pladsholdere er tilgængelige ved tilpasning af displaynavnet:

  • {indeks} vil blive erstattet med påkaldsindekset - simpelthen sagt, påkaldsindekset til den første udførelse er 1, for det andet er 2 osv.
  • {argumenter} er en pladsholder for den komplette, komma-adskilte liste over argumenter
  • {0}, {1}, ... er pladsholdere til individuelle argumenter

9. Konklusion

I denne artikel har vi undersøgt møtrikkerne og boltene til parametriserede tests i JUnit 5.

Vi lærte, at parametriserede tests er forskellige fra normale tests i to aspekter: de er kommenteret med @ParameterizedTest, og de har brug for en kilde til deres erklærede argumenter.

Også nu skal vi nu, at JUnit giver nogle faciliteter til at konvertere argumenterne til tilpassede måltyper eller til at tilpasse testnavne.

Som sædvanlig er prøvekoderne tilgængelige på vores GitHub-projekt, så sørg for at tjekke det ud!