Java >> Java tutorial >  >> Tag >> Json

Vejledning til ModelAssert-biblioteket til JSON

1. Oversigt

Når vi skriver automatiserede tests for software, der bruger JSON, er vi ofte nødt til at sammenligne JSON-data med en forventet værdi.

I nogle tilfælde kan vi behandle den faktiske og forventede JSON som strenge og udføre strengsammenligning, men dette har mange begrænsninger.

I denne vejledning vil vi se på, hvordan man skriver påstande og sammenligninger mellem JSON-værdier ved hjælp af ModelAssert. Vi vil se, hvordan man konstruerer påstande om individuelle værdier i et JSON-dokument, og hvordan man sammenligner dokumenter. Vi vil også dække, hvordan man håndterer felter, hvis nøjagtige værdier ikke kan forudsiges, såsom datoer eller GUID'er.

2. Kom godt i gang

ModelAssert er et datapåstandsbibliotek med en syntaks svarende til AssertJ og funktioner, der kan sammenlignes med JSONAssert. Den er baseret på Jackson til JSON-parsing og bruger JSON Pointer-udtryk til at beskrive stier til felter i dokumentet.

Lad os starte med at skrive nogle enkle påstande til denne JSON:

{
   "name": "Baeldung",
   "isOnline": true,
   "topics": [ "Java", "Spring", "Kotlin", "Scala", "Linux" ]
}

2.1. Afhængighed

Lad os for at starte med at tilføje ModelAssert til vores pom.xml :

<dependency>
    <groupId>uk.org.webcompere</groupId>
    <artifactId>model-assert</artifactId>
    <version>1.0.0</version>
    <scope>test</scope>
</dependency>

2.2. Angiv et felt i et JSON-objekt

Lad os forestille os, at eksemplet JSON er blevet returneret til os som en streng, og vi vil gerne kontrollere, at navnet felt er lig med Baeldung :

assertJson(jsonString)
  .at("/name").isText("Baeldung");

 assertJson metode vil læse JSON fra forskellige kilder, inklusive StringFilsti, og Jacksons JsonNode . Det returnerede objekt er en påstand, hvorpå vi kan bruge det flydende DSL (domænespecifikt sprog) til at tilføje betingelser.

Den at metoden beskriver et sted i dokumentet, hvor vi ønsker at fremsætte en feltpåstand. Derefter isText angiver, at vi forventer en tekstnode med værdien Baeldung .

Vi kan hævde en sti inden for emnerne array ved at bruge et lidt længere JSON Pointer-udtryk:

assertJson(jsonString)
  .at("/topics/1").isText("Spring");

Selvom vi kan skrive feltpåstande én efter én, kan vi også kombinere dem til en enkelt påstand :

assertJson(jsonString)
  .at("/name").isText("Baeldung")
  .at("/topics/1").isText("Spring");

2.3. Hvorfor strengsammenligning ikke virker

Ofte ønsker vi at sammenligne et helt JSON-dokument med et andet. Strengsammenligning, selvom det er muligt i nogle tilfælde, bliver ofte fanget af irrelevante JSON-formateringsproblemer :

String expected = loadFile(EXPECTED_JSON_PATH);
assertThat(jsonString)
  .isEqualTo(expected);

En fejlmeddelelse som denne er almindelig:

org.opentest4j.AssertionFailedError: 
expected: "{
    "name": "Baeldung",
    "isOnline": true,
    "topics": [ "Java", "Spring", "Kotlin", "Scala", "Linux" ]
}"
but was : "{"name": "Baeldung","isOnline": true,"topics": [ "Java", "Spring", "Kotlin", "Scala", "Linux" ]}"

2.4. Sammenligning af træer semantisk

For at lave en hel dokumentsammenligning kan vi bruge isEqualTo :

assertJson(jsonString)
  .isEqualTo(EXPECTED_JSON_PATH);

I dette tilfælde indlæses strengen af ​​den faktiske JSON af assertJson , og det forventede JSON-dokument – ​​en fil beskrevet af en sti – er indlæst i isEqualTo . Sammenligningen er lavet ud fra dataene.

2.5. Forskellige formater

ModelAssert understøtter også Java-objekter, der kan konverteres til JsonNode af Jackson, såvel som yaml format.

Map<String, String> map = new HashMap<>();
map.put("name", "baeldung");

assertJson(map)
  .isEqualToYaml("name: baeldung");

Til yaml håndtering, isEqualToYaml metode bruges til at angive formatet af strengen eller filen. Dette kræver assertYaml hvis kilden er yaml :

assertYaml("name: baeldung")
  .isEqualTo(map);

3. Feltpåstande

Indtil videre har vi set nogle grundlæggende påstande. Lad os se på mere af DSL.

3.1. Påstande ved enhver node

DSL for ModelAssert tillader, at næsten alle mulige betingelser tilføjes mod enhver node i træet. Dette skyldes, at JSON-træer kan indeholde noder af enhver type på ethvert niveau.

Lad os se på nogle påstande, vi kan tilføje til rodnoden i vores eksempel JSON:

assertJson(jsonString)
  .isNotNull()
  .isNotNumber()
  .isObject()
  .containsKey("name");

Da påstandsobjektet har disse metoder tilgængelige på sin grænseflade, vil vores IDE foreslå de forskellige påstande, vi kan tilføje i det øjeblik, vi trykker på “.” nøgle.

I dette eksempel har vi tilføjet en masse unødvendige betingelser, da den sidste betingelse allerede antyder et ikke-nul objekt.

Oftest bruger vi JSON Pointer-udtryk fra rodknuden for at udføre påstande på noder lavere nede i træet:

assertJson(jsonString)
  .at("/topics").hasSize(5);

Denne påstand bruger hasSize for at kontrollere, at arrayet i emnet felt har fem elementer. Den hasSize metoden fungerer på objekter, arrays og strenge. Et objekts størrelse er dets antal nøgler, en strengs størrelse er dets antal tegn, og et arrays størrelse er dets antal elementer.

De fleste påstande, vi skal fremsætte på felter, afhænger af den nøjagtige type af feltet. Vi kan bruge metoderne nummerarraytekstbooleanNode , og objekt at flytte ind i en mere specifik undergruppe af påstandene, når vi forsøger at skrive påstande om en bestemt type. Dette er valgfrit, men kan være mere udtryksfuldt:

assertJson(jsonString)
  .at("/isOnline").booleanNode().isTrue();

Når vi trykker på “.” indtast vores IDE efter booleanNode , ser vi kun autofuldførelsesmuligheder for booleske noder.

3.2. Tekstnode

Når vi hævder tekstnoder, kan vi bruge isText at sammenligne med en nøjagtig værdi. Alternativt kan vi bruge textContains for at hævde en understreng:

assertJson(jsonString)
  .at("/name").textContains("ael");

Vi kan også bruge regulære udtryk via matches :

assertJson(jsonString)
  .at("/name").matches("[A-Z].+");

Dette eksempel hævder, at navnet starter med stort bogstav.

3.3. Nummer Node

For nummernoder giver DSL nogle nyttige numeriske sammenligninger:

assertJson("{count: 12}")
  .at("/count").isBetween(1, 25);

Vi kan også angive den Java-numeriske type, vi forventer:

assertJson("{height: 6.3}")
  .at("/height").isGreaterThanDouble(6.0);

isEqualTo metoden er reserveret til matchning af hele træer, så til sammenligning af numerisk lighed bruger vi isNumberEqualTo :

assertJson("{height: 6.3}")
  .at("/height").isNumberEqualTo(6.3);

3.4. Array Node

Vi kan teste indholdet af et array med isArrayContaining :

assertJson(jsonString)
  .at("/topics").isArrayContaining("Scala", "Spring");

Dette tester for tilstedeværelsen af ​​de givne værdier og tillader det faktiske array at indeholde yderligere elementer. Hvis vi ønsker at hævde et mere præcist match, kan vi bruge isArrayContainingExactlyInAnyOrder :

assertJson(jsonString)
   .at("/topics")
   .isArrayContainingExactlyInAnyOrder("Scala", "Spring", "Java", "Linux", "Kotlin");

Vi kan også få dette til at kræve den nøjagtige rækkefølge:

assertJson(ACTUAL_JSON)
  .at("/topics")
  .isArrayContainingExactly("Java", "Spring", "Kotlin", "Scala", "Linux");

Dette er en god teknik til at hævde indholdet af arrays, der indeholder primitive værdier. Hvor et array indeholder objekter, vil vi måske bruge isEqualTo  i stedet.

4. Matching af hele træet

Selvom vi kan konstruere påstande med flere feltspecifikke betingelser for at tjekke, hvad der er i JSON-dokumentet, er vi ofte nødt til at sammenligne et helt dokument med et andet.

Den isEqualTo metode (eller isNotEqualTo ) bruges til at sammenligne hele træet. Dette kan kombineres med at for at flytte til et undertræ af det faktiske, før sammenligningen foretages:

assertJson(jsonString)
  .at("/topics")
  .isEqualTo("[ \"Java\", \"Spring\", \"Kotlin\", \"Scala\", \"Linux\" ]");

Sammenligning af hele træet kan ramme problemer, når JSON'en indeholder data, der enten er:

  • det samme, men i en anden rækkefølge
  • består af nogle værdier, der ikke kan forudsiges

Det hvor en metode bruges til at tilpasse den næste isEqualTo operation for at komme uden om disse.

4.1. Tilføj nøgleordrebegrænsning

Lad os se på to JSON-dokumenter, der virker ens:

String actualJson = "{a:{d:3, c:2, b:1}}";
String expectedJson = "{a:{b:1, c:2, d:3}}";

Vi skal bemærke, at dette ikke strengt taget er JSON-format. ModelAssert giver os mulighed for at bruge JavaScript-notationen til JSON , samt trådformatet, der normalt citerer feltnavnene.

Disse to dokumenter har nøjagtig de samme nøgler under “a” , men de er i en anden rækkefølge. En påstand om disse ville mislykkes, da ModelAssert som standard indstiller streng nøglerækkefølge .

Vi kan lempe nøgleordensreglen ved at tilføje en hvor konfiguration:

assertJson(actualJson)
  .where().keysInAnyOrder()
  .isEqualTo(expectedJson);

Dette tillader ethvert objekt i træet at have en anden rækkefølge af nøgler end det forventede dokument og stadig matche.

Vi kan lokalisere denne regel til en bestemt sti:

assertJson(actualJson)
  .where()
    .at("/a").keysInAnyOrder()
  .isEqualTo(expectedJson);

Dette begrænser keysInAnyOrder til kun "a" felt i rodobjektet.

Muligheden til at tilpasse sammenligningsreglerne giver os mulighed for at håndtere mange scenarier hvor det nøjagtige producerede dokument ikke kan kontrolleres eller forudsiges fuldt ud.

4.2. Afslappende array-begrænsninger

Hvis vi har arrays, hvor rækkefølgen af ​​værdier kan variere, så kan vi slække på array-rækkefølgen for hele sammenligningen:

String actualJson = "{a:[1, 2, 3, 4, 5]}";
String expectedJson = "{a:[5, 4, 3, 2, 1]}";

assertJson(actualJson)
  .where().arrayInAnyOrder()
  .isEqualTo(expectedJson);

Eller vi kan begrænse denne begrænsning til en sti, som vi gjorde med keysInAnyOrder .

4.3. Ignorer stier

Måske indeholder vores egentlige dokument nogle felter, der enten er uinteressante eller uforudsigelige. Vi kan tilføje en regel for at ignorere stien:

String actualJson = "{user:{name: \"Baeldung\", url:\"http://www.baeldung.com\"}}";
String expectedJson = "{user:{name: \"Baeldung\"}}";

assertJson(actualJson)
  .where()
    .at("/user/url").isIgnored()
  .isEqualTo(expectedJson);

Vi bør bemærke, at den vej, vi udtrykker, altid er i form af JSON-pegeren inden for den faktiske .

Det ekstra felt “url” i den faktiske er nu ignoreret.

4.4. Ignorer enhver GUID

Indtil videre har vi kun tilføjet regler ved hjælp af at for at tilpasse sammenligning på bestemte steder i dokumentet.

stien syntaks giver os mulighed for at beskrive, hvor vores regler gælder ved hjælp af jokertegn. Når vi tilføjer en at eller sti tilstand til hvor af vores sammenligning kan vi også levere en hvilken som helst af feltpåstandene ovenfra til brug i stedet for en side-by-side sammenligning med det forventede dokument.

Lad os sige, at vi havde et id felt, der dukkede op flere steder i vores dokument og var en GUID, som vi ikke kunne forudsige.

Vi kunne ignorere dette felt med en stiregel:

String actualJson = "{user:{credentials:[" +
  "{id:\"a7dc2567-3340-4a3b-b1ab-9ce1778f265d\",role:\"Admin\"}," +
  "{id:\"09da84ba-19c2-4674-974f-fd5afff3a0e5\",role:\"Sales\"}]}}";
String expectedJson = "{user:{credentials:" +
  "[{id:\"???\",role:\"Admin\"}," +
  "{id:\"???\",role:\"Sales\"}]}}";

assertJson(actualJson)
  .where()
    .path("user","credentials", ANY, "id").isIgnored()
  .isEqualTo(expectedJson);

Her kan vores forventede værdi have alt for id felt, fordi vi simpelthen har ignoreret ethvert felt, hvis JSON Pointer starter “/user/credentials” har derefter en enkelt node (array-indekset) og ender på “/id” .

4.5. Match enhver GUID

At ignorere felter, vi ikke kan forudsige, er én mulighed. Det er bedre i stedet at matche disse noder efter type, og måske også efter en anden betingelse, de skal opfylde. Lad os skifte til at tvinge disse GUID'er til at matche mønsteret af en GUID, og ​​lad os tillade id node, der vises ved enhver bladknude i træet:

assertJson(actualJson)
  .where()
    .path(ANY_SUBTREE, "id").matches(GUID_PATTERN)
  .isEqualTo(expectedJson);

 ANY_SUBTREE jokertegn matcher et vilkårligt antal noder mellem dele af stiudtrykket. GUID_PATTERN kommer fra ModelAssert Patterns klasse, som indeholder nogle almindelige regulære udtryk for at matche ting som tal og datostempler.

4.6. Tilpasning af isEqualTo

Kombinationen af hvor med enten sti eller  udtryk giver os mulighed for at tilsidesætte sammenligninger overalt i træet. Vi tilføjer enten de indbyggede regler for et objekt- eller matrixmatchning eller specificerer specifikke alternative påstande til brug for individuelle stier eller klasser af stier i sammenligningen.

Hvor vi har en fælles konfiguration, genbrugt på tværs af forskellige sammenligninger, kan vi udtrække den til en metode:

private static <T> WhereDsl<T> idsAreGuids(WhereDsl<T> where) {
    return where.path(ANY_SUBTREE, "id").matches(GUID_PATTERN);
}

Derefter kan vi tilføje denne konfiguration til en bestemt påstand med configuredBy :

assertJson(actualJson)
  .where()
    .configuredBy(where -> idsAreGuids(where))
  .isEqualTo(expectedJson);

5. Kompatibilitet med andre biblioteker

ModelAssert blev bygget til interoperabilitet. Indtil videre har vi set AssertJ-stilpåstandene. Disse kan have flere betingelser, og de vil fejle på den første betingelse, der ikke er opfyldt.

Men nogle gange er vi nødt til at producere et matcherobjekt til brug med andre typer test.

5.1. Hamcrest Matcher

Hamcrest er et stort påstandshjælpebibliotek, der understøttes af mange værktøjer. Vi kan bruge DSL fra ModelAssert til at producere en Hamcrest-matcher :

Matcher<String> matcher = json()
  .at("/name").hasValue("Baeldung");

 json metode bruges til at beskrive en matcher, der accepterer en streng med JSON-data i det. Vi kunne også bruge jsonFile at producere en Matcher der forventer at hævde indholdet af en filJsonAssertions klasse i ModelAssert indeholder flere builder-metoder som denne for at begynde at bygge en Hamcrest-matcher.

DSL til at udtrykke sammenligningen er identisk med assertJson , men sammenligningen udføres ikke før noget bruger matcheren.

Vi kan derfor bruge ModelAssert med Hamcrests MatcherAssert :

MatcherAssert.assertThat(jsonString, json()
  .at("/name").hasValue("Baeldung")
  .at("/topics/1").isText("Spring"));

5.2. Brug med Spring Mock MVC

Mens vi bruger svartekstbekræftelse i Spring Mock MVC, kan vi bruge Springs indbyggede jsonPath påstande. Foråret giver os dog også mulighed for at bruge Hamcrest-matchere til at hævde den streng, der returneres som svarindhold. Dette betyder, at vi kan udføre sofistikerede indholdspåstande ved hjælp af ModelAssert.

5.3. Brug med Mockito

Mockito er allerede interoperabel med Hamcrest. ModelAssert giver dog også en indbygget ArgumentMatcher . Dette kan bruges både til at konfigurere stubs adfærd og til at bekræfte opkald til dem:

public interface DataService {
    boolean isUserLoggedIn(String userDetails);
}

@Mock
private DataService mockDataService;

@Test
void givenUserIsOnline_thenIsLoggedIn() {
    given(mockDataService.isUserLoggedIn(argThat(json()
      .at("/isOnline").isTrue()
      .toArgumentMatcher())))
      .willReturn(true);

    assertThat(mockDataService.isUserLoggedIn(jsonString))
      .isTrue();

    verify(mockDataService)
      .isUserLoggedIn(argThat(json()
        .at("/name").isText("Baeldung")
        .toArgumentMatcher()));
}

I dette eksempel er Mockito argThat bruges både i opsætningen af ​​en mock og bekræft . Inden i det bruger vi Hamcrest-stilbyggeren til matcheren – json . Derefter tilføjer vi betingelser til det, og konverterer til Mockitos ArgumentMatcher til sidst med toArgumentMatcher .

6. Konklusion

I denne artikel har vi set på behovet for at sammenligne JSON semantisk i vores test.

Vi så, hvordan ModelAssert kan bruges til at bygge en påstand på individuelle noder i et JSON-dokument såvel som hele træer. Derefter så vi, hvordan man tilpasser træsammenligning for at tillade uforudsigelige eller irrelevante forskelle.

Til sidst så vi, hvordan man bruger ModelAssert med Hamcrest og andre biblioteker.

Som altid er eksempelkoden fra denne tutorial tilgængelig på GitHub.


Java tag