Java >> Java Tutorial >  >> Tag >> Spring

Integrationstests von Spring MVC-Anwendungen:Schreiben Sie saubere Behauptungen mit JsonPath

In den vorherigen Teilen meines Spring MVC Test-Tutorials wurde beschrieben, wie wir Integrationstests für eine REST-API schreiben können. Obwohl die in diesen Blogbeiträgen beschriebenen Techniken nützlich sind, besteht das Problem darin, dass unsere Behauptungen nicht sehr elegant waren. Unsere Behauptungen stellten im Wesentlichen sicher, dass der Hauptteil der HTTP-Antwort die „richtigen“ Zeichenfolgen enthielt.

Dieser Ansatz hat zwei Probleme:

  • Es ist nicht sehr lesbar, besonders wenn das zurückgegebene JSON groß ist. Da unsere Tests als Dokumentation für unseren Code dienen sollen, ist dies ein großes Problem.
  • Es ist sehr schwierig, Tests zu schreiben, die sicherstellen, dass die Reihenfolge der Sammlungen korrekt ist, ohne die Lesbarkeit zu beeinträchtigen.

Zum Glück für uns gibt es einen besseren Weg, dies zu tun. JsonPath ist für JSON das, was XPath für XML ist. Es bietet eine einfache und lesbare Möglichkeit, Teile eines JSON-Dokuments zu extrahieren. Dieser Blogbeitrag beschreibt, wie wir Assertionen schreiben können, indem wir den Spring MVC Test und die Java-Implementierung von JsonPath verwenden.

Fangen wir an und finden heraus, wie wir die erforderlichen Abhängigkeiten mit Maven erhalten können.

Erforderliche Abhängigkeiten mit Maven erhalten

Wir können die erforderlichen Abhängigkeiten mit Maven erhalten, indem wir diesen Schritten folgen:

  1. Deklarieren Sie die Hamcrest-Abhängigkeit (Version 1.3) in der pom.xml Datei.
  2. Deklarieren Sie die JUnit-Abhängigkeit (Version 4.11) in der pom.xml Datei und schließen Sie die Hamcrest-Abhängigkeiten aus.
  3. Deklarieren Sie die Abhängigkeit von Spring Test (Version 3.2.2.RELEASE) in pom.xml Datei.
  4. JsonPath-Abhängigkeiten (Version 0.8.1) zur pom.xml-Datei hinzufügen .

Die entsprechenden Abhängigkeitserklärungen sehen wie folgt aus:

<dependency>
	<groupId>org.hamcrest</groupId>
	<artifactId>hamcrest-all</artifactId>
	<version>1.3</version>
	<scope>test</scope>
</dependency>
<dependency>
	<groupId>junit</groupId>
	<artifactId>junit</artifactId>
	<version>4.11</version>
	<scope>test</scope>
	<exclusions>
		<exclusion>
			<artifactId>hamcrest-core</artifactId>
			<groupId>org.hamcrest</groupId>
		</exclusion>
	</exclusions>
</dependency>
<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-test</artifactId>
	<version>3.2.2.RELEASE</version>
	<scope>test</scope>
</dependency>
<dependency>
	<groupId>com.jayway.jsonpath</groupId>
	<artifactId>json-path</artifactId>
	<version>0.8.1</version>
	<scope>test</scope>
</dependency>
<dependency>
	<groupId>com.jayway.jsonpath</groupId>
	<artifactId>json-path-assert</artifactId>
	<version>0.8.1</version>
	<scope>test</scope>
</dependency>

Lassen Sie uns umziehen und herausfinden, wie wir Assertionen mit JsonPath schreiben können.

Integrationstests schreiben

Wir können Integrationstests schreiben, indem wir die getestete Operation ausführen und Behauptungen für das zurückgegebene JSON-Dokument erstellen. Wir können eine neue Assertion erstellen, indem wir diesen Schritten folgen:

  1. Erstellen Sie einen JsonPath-Ausdruck, der den bevorzugten Teil aus dem zurückgegebenen JSON-Dokument abruft (Weitere Informationen zur JsonPath-Notation erhalten).
  2. Stellen Sie mit einem Hamcrest-Matcher eine Behauptung gegen den abgerufenen Teil auf.
  3. Verwenden Sie die jsonPath()-Methode der MockMvcResultMatchers-Klasse, um zu überprüfen, ob die Behauptung wahr ist, und übergeben Sie die in den Phasen eins und zwei erstellten Objekte als Methodenparameter.

Genug der Theorie. Lassen Sie uns weitermachen und herausfinden, wie wir Behauptungen gegen JSON-Dokumente schreiben können. Die folgenden Unterabschnitte beschreiben, wie wir Behauptungen gegen JSON-Dokumente schreiben können, die entweder die Informationen eines einzelnen Objekts oder die Informationen mehrerer Objekte enthalten.

Einzelnes Objekt

Dieser Unterabschnitt beschreibt, wie wir sicherstellen können, dass das JSON-Dokument, das die Informationen eines einzelnen Objekts enthält, korrekt ist. Als Beispiel schreiben wir einen Integrationstest für eine Controller-Methode, mit der die Informationen eines bestehenden Todo-Eintrags gelöscht werden. Nachdem der ToDo-Eintrag erfolgreich gelöscht wurde, werden seine Informationen an den Client zurückgesendet. Das zurückgegebene JSON sieht wie folgt aus:

{
	"id":1,
	"description":"Lorem ipsum",
	"title":"Foo"
}

Wir können den Integrationstest schreiben, indem wir diesen Schritten folgen:

  1. Verwenden Sie die @ExpectedDatabase Anmerkung, um sicherzustellen, dass der Aufgabeneintrag gelöscht wird.
  2. Führen Sie eine DELETE-Anforderung an die URL „/api/todo/1“ durch. Legen Sie den angemeldeten Benutzer fest.
  3. Vergewissern Sie sich, dass der zurückgegebene HTTP-Statuscode 200 ist.
  4. Stellen Sie sicher, dass der Inhaltstyp der Antwort „application/json“ und der Zeichensatz „UTF-8“ ist.
  5. Rufen Sie die ID des gelöschten Aufgabeneintrags mithilfe des JsonPath-Ausdrucks $.id ab und vergewissern Sie sich, dass die ID 1 ist.
  6. Rufen Sie die Beschreibung des gelöschten Aufgabeneintrags mit dem JsonPath-Ausdruck $.description ab und vergewissern Sie sich, dass die Beschreibung "Lorem ipsum" lautet.
  7. Rufen Sie den Titel des gelöschten Aufgabeneintrags ab, indem Sie den JsonPath-Ausdruck $.title verwenden und vergewissern Sie sich, dass der Titel "Foo" lautet.

Der Quellcode unseres Integrationstests sieht wie folgt aus:

import com.github.springtestdbunit.DbUnitTestExecutionListener;
import com.github.springtestdbunit.annotation.DatabaseSetup;
import com.github.springtestdbunit.annotation.ExpectedDatabase;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
import org.springframework.test.web.server.MockMvc;
import org.springframework.test.web.server.samples.context.WebContextLoader;

import static org.hamcrest.Matchers.*;
import static org.springframework.test.web.server.samples.context.SecurityRequestPostProcessors.userDetailsService;
import static org.springframework.test.web.server.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.server.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.server.result.MockMvcResultMatchers.status;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader = WebContextLoader.class, classes = {ExampleApplicationContext.class})
@TestExecutionListeners({DependencyInjectionTestExecutionListener.class,
        DirtiesContextTestExecutionListener.class,
        TransactionalTestExecutionListener.class,
        DbUnitTestExecutionListener.class})
@DatabaseSetup("toDoData.xml")
public class ITTodoControllerTest {

    //Add web application context here

    private MockMvc mockMvc;

    //Add setUp() method here

    @Test
    @ExpectedDatabase("toDoData-delete-expected.xml")
    public void deleteById() throws Exception {
        mockMvc.perform(delete("/api/todo/{id}", 1L)
                .with(userDetailsService(IntegrationTestUtil.CORRECT_USERNAME))
        )
                .andExpect(status().isOk())
                .andExpect(content().mimeType(IntegrationTestUtil.APPLICATION_JSON_UTF8))
                .andExpect(jsonPath("$.id", is(1)))
                .andExpect(jsonPath("$.description", is("Lorem ipsum")))
                .andExpect(jsonPath("$.title", is("Foo")));
    }
}

Sammlung von Objekten

Dieser Unterabschnitt beschreibt, wie wir Zusicherungen schreiben können, die sicherstellen, dass das JSON-Dokument, das eine Sammlung von Objekten enthält, korrekt ist. Wir werden uns zwei verschiedene Situationen ansehen:

  • Die Objekte werden immer in der gleichen Reihenfolge zurückgegeben.
  • Die Objekte werden in zufälliger Reihenfolge zurückgegeben.

Lasst uns unsere Reise fortsetzen.

Objekte werden in derselben Reihenfolge zurückgegeben

Wenn der Benutzer alle Todo-Einträge abrufen möchte, die in der Datenbank gespeichert sind, werden die Einträge immer in der gleichen Reihenfolge zurückgegeben. Das zurückgegebene JSON sieht wie folgt aus:

[
	{
		"id":1,
		"description":"Lorem ipsum",
		"title":"Foo"
	},
	{
		"id":2,
		"description":"Lorem ipsum",
		"title":"Bar"
	}
]

Wir können unseren Integrationstest schreiben, indem wir diesen Schritten folgen:

  1. Verwenden Sie die @ExpectedDatabase Anmerkung, um sicherzustellen, dass keine Änderungen an der Datenbank vorgenommen wurden.
  2. Führen Sie eine GET-Anforderung an die URL „/api/todo“ durch. Legen Sie den angemeldeten Benutzer fest.
  3. Vergewissern Sie sich, dass der zurückgegebene HTTP-Statuscode 200 ist.
  4. Stellen Sie sicher, dass der Inhaltstyp der Antwort „application/json“ und ihr Zeichensatz „UTF-8“ ist.
  5. Rufen Sie die Sammlung von Aufgabeneinträgen mithilfe des JsonPath-Ausdrucks $ ab und stellen Sie sicher, dass zwei Aufgabeneinträge zurückgegeben werden.
  6. Verwenden Sie die JsonPath-Ausdrücke $[0].id , $[0].Beschreibung und $[0].title um die ID, Beschreibung und den Titel des ersten Aufgabeneintrags zu erhalten. Überprüfen Sie, ob die Informationen korrekt sind.
  7. Verwenden Sie die JsonPath-Ausdrücke $[1].id , $[1].Beschreibung und $[1].title um die ID, Beschreibung und den Titel des zweiten Todo-Eintrags zu erhalten. Überprüfen Sie, ob die Informationen korrekt sind.

Der Quellcode unseres Integrationstests sieht wie folgt aus:

import com.github.springtestdbunit.DbUnitTestExecutionListener;
import com.github.springtestdbunit.annotation.DatabaseSetup;
import com.github.springtestdbunit.annotation.ExpectedDatabase;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
import org.springframework.test.web.server.MockMvc;
import org.springframework.test.web.server.samples.context.WebContextLoader;

import static org.hamcrest.Matchers.*;
import static org.springframework.test.web.server.samples.context.SecurityRequestPostProcessors.userDetailsService;
import static org.springframework.test.web.server.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.server.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.server.result.MockMvcResultMatchers.status;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader = WebContextLoader.class, classes = {ExampleApplicationContext.class})
@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class,
        DirtiesContextTestExecutionListener.class,
        TransactionalTestExecutionListener.class,
        DbUnitTestExecutionListener.class })
@DatabaseSetup("toDoData.xml")
public class ITTodoControllerTest {

    //Add web application context here

    private MockMvc mockMvc;

    //Add setUp() method here

    @Test
    @ExpectedDatabase("toDoData.xml")
    public void findAll() throws Exception {
        mockMvc.perform(get("/api/todo")
                .with(userDetailsService(IntegrationTestUtil.CORRECT_USERNAME))
        )
                .andExpect(status().isOk())
                .andExpect(content().mimeType(IntegrationTestUtil.APPLICATION_JSON_UTF8))
				.andExpect(jsonPath("$", hasSize(2)))
				.andExpect(jsonPath("$[0].id", is(1)))
				.andExpect(jsonPath("$[0].description", is("Lorem ipsum")))
				.andExpect(jsonPath("$[0].title", is("Foo")))
				.andExpect(jsonPath("$[1].id", is(2)))
				.andExpect(jsonPath("$[1].description", is("Lorem ipsum")))
				.andExpect(jsonPath("$[1].title", is("Bar")));
    }
}

Objekte, die in zufälliger Reihenfolge zurückgegeben werden

Wenn eine Validierung eines hinzugefügten oder aktualisierten Aufgabeneintrags fehlschlägt, gibt unsere Beispielanwendung die Feldfehler an den Client unserer REST-API zurück. Das Problem ist, dass wir die Reihenfolge, in der die Felder validiert werden, nicht garantieren können. Das bedeutet, dass die Feldfehler in zufälliger Reihenfolge zurückgegeben werden. Ein JSON-Dokument, das die zurückgegebenen Feldfehler enthält, sieht folgendermaßen aus:

{
	"fieldErrors":[
		{
			"path":"description",
			"message":"The maximum length of the description is 500 characters."
		},
		{
			"path":"title",
			"message":"The maximum length of the title is 100 characters."
		}
	]
}

Wir können einen Integrationstest schreiben, der überprüft, ob Feldfehler zurückgegeben werden, wenn ein neuer Aufgabeneintrag mit ungültigen Informationen hinzugefügt wird, indem Sie die folgenden Schritte ausführen:

  1. Verwenden Sie die @ExpectedDatabase Anmerkung, um sicherzustellen, dass keine Änderungen an der Datenbank vorgenommen wurden.
  2. Erstellen Sie den Titel und die Beschreibung des Aufgabeneintrags. Stellen Sie sicher, dass sowohl der Titel als auch die Beschreibung zu lang sind.
  3. Erstellen Sie ein neues TodoDTO Objekt und legen Sie seinen Titel und seine Beschreibung fest.
  4. Führen Sie eine POST-Anfrage an die URL „/api/todo“ durch. Legen Sie den Inhaltstyp der Anfrage auf „application/json“ fest. Stellen Sie den Zeichensatz der Anfrage auf „UTF-8“ ein. Wandeln Sie das erstellte Objekt in ein korrektes Format um und senden Sie es im Hauptteil der Anfrage. Legen Sie den angemeldeten Benutzer fest.
  5. Stellen Sie sicher, dass der Inhaltstyp der Antwort „application/json“ und der Zeichensatz „UTF-8“ ist.
  6. Rufen Sie die Feldfehler mit dem JsonPath-Ausdruck $.fieldErrors ab und stellen Sie sicher, dass zwei Feldfehler zurückgegeben werden.
  7. Verwenden Sie den JsonPath-Ausdruck $.fieldErrors[*].path um alle verfügbaren Pfade abzurufen. Stellen Sie sicher, dass Feldfehler zu Titel- und Beschreibungsfeldern verfügbar sind.
  8. Verwenden Sie den JsonPath-Ausdruck $.fieldErrors[*].message um alle verfügbaren Fehlermeldungen abzurufen. Stellen Sie sicher, dass Fehlermeldungen zu Titel- und Beschreibungsfeldern zurückgegeben werden.

Der Quellcode unseres Integrationstests sieht wie folgt aus:

import com.github.springtestdbunit.DbUnitTestExecutionListener;
import com.github.springtestdbunit.annotation.DatabaseSetup;
import com.github.springtestdbunit.annotation.ExpectedDatabase;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
import org.springframework.test.web.server.MockMvc;
import org.springframework.test.web.server.samples.context.WebContextLoader;

import static org.hamcrest.Matchers.*;
import static org.springframework.test.web.server.samples.context.SecurityRequestPostProcessors.userDetailsService;
import static org.springframework.test.web.server.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.server.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.server.result.MockMvcResultMatchers.status;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader = WebContextLoader.class, classes = {ExampleApplicationContext.class})
@TestExecutionListeners({DependencyInjectionTestExecutionListener.class,
        DirtiesContextTestExecutionListener.class,
        TransactionalTestExecutionListener.class,
        DbUnitTestExecutionListener.class})
@DatabaseSetup("toDoData.xml")
public class ITTodoControllerTest {

    //Add web application context here

    private MockMvc mockMvc;

    //Add setUp() method here

    @Test
    @ExpectedDatabase("toDoData.xml")
    public void addTodoWhenTitleAndDescriptionAreTooLong() throws Exception {
        String title = TodoTestUtil.createStringWithLength(101);
        String description = TodoTestUtil.createStringWithLength(501);
        TodoDTO added = TodoTestUtil.createDTO(null, description, title);

        mockMvc.perform(post("/api/todo")
                .contentType(IntegrationTestUtil.APPLICATION_JSON_UTF8)
                .body(IntegrationTestUtil.convertObjectToJsonBytes(added))
                .with(userDetailsService(IntegrationTestUtil.CORRECT_USERNAME))
        )
                .andExpect(status().isBadRequest())
                .andExpect(content().mimeType(IntegrationTestUtil.APPLICATION_JSON_UTF8))
                .andExpect(jsonPath("$.fieldErrors", hasSize(2)))
				.andExpect(jsonPath("$.fieldErrors[*].path", containsInAnyOrder("title", "description")))
				.andExpect(jsonPath("$.fieldErrors[*].message", containsInAnyOrder(
						"The maximum length of the description is 500 characters.",
						"The maximum length of the title is 100 characters."
				)));                
    }
}

Zusammenfassung

Wir haben jetzt Integrationstests für eine REST-API geschrieben, indem wir Spring MVC Test und JsonPath verwendet haben. Dieser Blogbeitrag hat uns vier Dinge gelehrt:

  • Wir haben gelernt, wie wir die erforderlichen JsonPath-Abhängigkeiten mit Maven erhalten können.
  • Wir haben gelernt, wie wir Behauptungen gegen die JSON-Darstellung eines einzelnen Objekts schreiben können.
  • Wir haben gelernt, wie wir Behauptungen gegen die JSON-Darstellung einer Sammlung von Objekten schreiben können.
  • Wir haben gelernt, dass das Schreiben von Behauptungen mit JsonPath die Lesbarkeit unserer Tests verbessert.

P.S. Sie können die Beispielanwendung dieses Blogbeitrags von Github herunterladen.


Java-Tag