[JAVA] Eine Geschichte, der ich beim Testen der API mit MockMVC verfallen war

Entwicklungsumgebung

Hintergrund

Ich habe beschlossen, "RestController" zu testen, aber da es schwierig ist, die Anwendung bei jedem Test bereitzustellen, habe ich mich entschlossen, "Mock" zu testen. Wenn es darum geht, mit "Mock" eine mit "Spring" entwickelte Anwendung zu testen, gibt es verschiedene Methoden und Bibliotheken, aber dieses Mal werden wir "MockMVC" verwenden.

Was ist MockMVC?

Als eine der Funktionen von "Spring Test" können Sie das Verhalten von "Spring MVC" reproduzieren, ohne die Anwendung auf dem Server bereitzustellen. Es gibt zwei Möglichkeiten, MockMVC zu verwenden: Die eine besteht darin, den DI-Container für Webanwendungen ( WebApplicationContext) zu verwenden, und die andere darin, den durch Spring Test generierten DI-Container zu verwenden. Dieses Mal verwenden wir den ersteren: grinsen :.

Einzelheiten entnehmen Sie bitte den TERASOLUNA-Richtlinien. Verwendung der für Unit-Tests verwendeten OSS-Bibliothek - Was ist MockMvc-

Test durchführen

Zu testender Code

Nehmen wir als Beispiel die Todo-Anwendung von TERASOLUNA (REST-Edition). Die zu testende API ist "Post Todos", die Todo erstellt. Wenn Sie "Title" auf das Objekt "TodoResource" und "post" im JSON-Format setzen, werden die ID und das Datum angegeben und die unvollständige "TodoResource" zurückgegeben.

Pakete, Importe, Getter & Setter werden weggelassen. Der zu testende Code ist in diesem Artikel nicht wichtig, daher werde ich Dinge wie "Todo.java" und "TodoService.java" weglassen.

TodoRestController.java


@RestController
@RequestMapping("todos") 
public class TodoRestController {
    @Inject
    TodoService todoService;
    @Inject
    Mapper beanMapper;

    @RequestMapping(method = RequestMethod.POST)
    @ResponseStatus(HttpStatus.CREATED)
    public TodoResource postTodos(@RequestBody @Validated TodoResource todoResource) {
        Todo createdTodo = todoService.create(beanMapper.map(todoResource, Todo.class));
        TodoResource createdTodoResponse = beanMapper.map(createdTodo, TodoResource.class);
        return createdTodoResponse;
    }
}

TodoResource.java


public class TodoResource implements Serializable {

    private static final long serialVersionUID = 1L;

    private String todoId;

    @NotNull
    @Size(min = 1, max = 30)
    private String todoTitle;

    private boolean finished;

    private Date createdAt;
}

TodoServiceImpl.java


@Service
@Transactional
public class TodoServiceImpl implements TodoService {
	private static final long MAX_UNFINISHED_COUNT = 5;

	@Inject
	TodoRepository todoRepository;
	@Override
	public Todo create(Todo todo) {
		long unfinishedCount = todoRepository.countByFinished(false);
		if (unfinishedCount >= MAX_UNFINISHED_COUNT) {
			ResultMessages messages = ResultMessages.error();
            messages.add("E001", MAX_UNFINISHED_COUNT);
            throw new BusinessException(messages);
		}

		String todoId = UUID.randomUUID().toString();
		Date createdAt = new Date();

		todo.setTodoId(todoId);
		todo.setCreatedAt(createdAt);
		todo.setFinished(false);

		todoRepository.create(todo);

		return todo;
	}
}

web.xml


<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    version="3.0">
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <!-- Root ApplicationContext -->
        <param-value>
            classpath*:META-INF/spring/applicationContext.xml
            classpath*:META-INF/spring/spring-security.xml
        </param-value>
    </context-param>

    <!--Die Definition verschiedener Filter entfällt-->

    <servlet>
        <servlet-name>restApiServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <!-- ApplicationContext for Spring MVC (REST) -->
            <param-value>classpath*:META-INF/spring/spring-mvc-rest.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>restApiServlet</servlet-name>
        <url-pattern>/api/v1/*</url-pattern>
    </servlet-mapping>
Implementierung von Testcode

Bestätigen Sie, dass das generierte "TodoResource" -Objekt zurückgegeben wird.

TodoRestControllerTest.java


@RunWith(SpringRunner.class)
@ContextHierarchy({@ContextConfiguration({"classpath:META-INF/spring/applicationContext.xml"}),
    @ContextConfiguration({"classpath:META-INF/spring/spring-mvc-rest.xml"})})
@WebAppConfiguration
public class TodoRestControllerTest {

  @Autowired
  WebApplicationContext webApplicationContext;

  MockMvc mockMvc;

  ObjectMapper mapper;

  @Before
  public void setup() {
    mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).alwaysDo(log()).build();
    mapper = new ObjectMapper();
  }

  @Test
  public void postTodoTest() throws Exception {
    String title = "title";
    TodoResource todoRequest = new TodoResource();
    todoRequest.setTodoTitle(title);
    MvcResult result =
        mockMvc
            .perform(MockMvcRequestBuilders.post("/api/v1/todos")
                .content(mapper.writeValueAsString(todoRequest))
                .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
            .andExpect(status().isCreated()).andReturn();

    TodoResource todoResponce =
        mapper.readValue(result.getResponse().getContentAsString(), TodoResource.class);
    assertThat(todoResponce.getTodoId(), notNullValue());
    assertThat(todoResponce.getTodoTitle(), equalTo(title));
    assertThat(todoResponce.isFinished(), equalTo(false));
    assertThat(todoResponce.getCreatedAt(), notNullValue());
  }
}
Führen Sie den Testcode aus

Wenn ich es ausführe, ist der Statuscode OK (200) anstelle von Erstellt (201) ...: müde:

java.lang.AssertionError: Status expected:<201> but was:<200>
	at org.springframework.test.util.AssertionErrors.fail(AssertionErrors.java:54)
	at org.springframework.test.util.AssertionErrors.assertEquals(AssertionErrors.java:81)
	at org.springframework.test.web.servlet.result.StatusResultMatchers$10.match(StatusResultMatchers.java:665)
	at org.springframework.test.web.servlet.MockMvc$1.andExpect(MockMvc.java:171)
	at todo.api.TodoRestControllerTest.postTodoTest(TodoRestControllerTest.java:55)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
	at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
	at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
	at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:252)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:94)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
	at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:191)
	at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
	at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:538)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:760)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:460)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:206)

Übrigens, wenn Sie tatsächlich auf dem Server bereitstellen und "Postman" verwenden, um "POST" an "/ api / v1 / todos" zu senden, wird "Todo" erstellt und 201 zurückgegeben.

postmanでの結果

Danach habe ich sorgfältig geprüft, ob es keine Fehler in der "URL" und den Einstellungen gab, sondern nur die verstrichene Zeit ...

Lösungen

Untersuchen Sie TestDispatcherServlet

Lesen Sie Verwendung der für Unit-Tests verwendeten OSS-Bibliothek - Was ist MockMvc- Wie ich bemerkt habe, scheint MockMVC TestDispatcherServlet` zu verwenden, um eine Pseudoanforderung zu erfüllen. Schauen wir uns hier tatsächlich TestDispatcherServlet.java an. TestDispatcherServlet.java-4.3.x-

TestDispatcherServlet.java


final class TestDispatcherServlet extends DispatcherServlet {

	private static final String KEY = TestDispatcherServlet.class.getName() + ".interceptor";


	/**
	 * Create a new instance with the given web application context.
	 */
	public TestDispatcherServlet(WebApplicationContext webApplicationContext) {
		super(webApplicationContext);
	}

Sie rufen den Konstruktor der übergeordneten Klasse "DispatcherServlet" im Konstruktor auf. Schauen wir uns als nächstes DispatcherServlet.java an.

DispatcherServlet.java-4.3.x-

DispatcherServlet.java


public class DispatcherServlet extends FrameworkServlet {

	/**
	 * Create a new {@code DispatcherServlet} with the given web application context. This
	 * constructor is useful in Servlet 3.0+ environments where instance-based registration
	 * of servlets is possible through the {@link ServletContext#addServlet} API.
	 * <p>Using this constructor indicates that the following properties / init-params
	 * will be ignored:
	 * ~Abkürzung~
	 */
	public DispatcherServlet(WebApplicationContext webApplicationContext) {
		super(webApplicationContext);
		setDispatchOptionsRequest(true);
	}

Es heißt "Erstellen Sie ein" DispatcherServlet "im angegebenen Anwendungskontext." Schauen wir uns auch den Konstruktor ohne Argumente über diesem Code an

DispatcherServlet.java-4.3.x-

DispatcherServlet.java


public class DispatcherServlet extends FrameworkServlet {

	/**
	 * Create a new {@code DispatcherServlet} that will create its own internal web
	 * application context based on defaults and values provided through servlet
	 * init-params. Typically used in Servlet 2.5 or earlier environments, where the only
	 * option for servlet registration is through {@code web.xml} which requires the use
	 * of a no-arg constructor.
	 * ~Abkürzung~
	 */
	public DispatcherServlet(WebApplicationContext webApplicationContext) {
		super(webApplicationContext);
		setDispatchOptionsRequest(true);
	}

Neben "Erstellen Sie ein neues" DispatcherServlet ", das Ihren eigenen internen Webanwendungskontext bereitstellt.", Dann "Die einzige Möglichkeit, ein Servlet zu registrieren, ist" web.xml ", für die ein Konstruktor ohne Argumente erforderlich ist. "(Freie Übersetzung). Mit anderen Worten, das von MockMVC verwendete TestDispatcherServlet ruft den Konstruktor mit dem Argument DispatcherServlet auf, sodass der Beschreibungsinhalt von web.xml nicht wiedergegeben wird. Bedeutet das? Nach Wörtern suchen, die für die Testversion relevant zu sein scheinen, ist die Version von "Spring Framework" 3.2, aber einige Leute fragten, wie man "web.xml" in einem Test mit "MockMvc" lädt. ..

Does the new Spring MVC Test Framework released in Spring 3.2 test the web.xml configuration?

Die Befragten sagen, dass "Spring-mvc-test die Datei web.xml nicht liest".

Testfix

Wenn die in "web.xml" beschriebenen Inhalte nicht wiedergegeben werden, bedeutet dies, dass "Servlet-Mapping" nicht funktioniert, sodass der tatsächliche Pfad "/ todos" anstelle von "/ api / v1 / todos" lautet. .. Korrigieren wir also den Testcode und versuchen es erneut.

TodoRestControllerTest.java


    MvcResult result = mockMvc
        .perform(
            MockMvcRequestBuilders.post("/todos").content(mapper.writeValueAsString(todoRequest))
                .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
        .andExpect(status().isCreated()).andReturn();

result.PNG

Ich habs gemacht!

Ich möchte den Inhalt von web.xml im Testcode wiedergeben ...

Es war gut, dass es mit ↑ funktioniert hat, aber es fühlt sich etwas unangenehm an, dass der tatsächliche Anwendungspfad "/ api / v1 / todos" und der Testpfad "/ todos" ist. Ich weiß, dass Filter mit addFilter wie folgt hinzugefügt werden können: sos :.

TodoRestControllerTest.java


    mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
        .addFilter(new XTrackMDCPutFilter(), "/**").alwaysDo(log()).build();

Hintergrund zum Posting

Der in diesem Artikel beschriebene Code stammt aus dem Beispiel, aber in der tatsächlichen Entwicklung sollte die URL-Spezifikation korrekt sein, aber es wurde 404, und ich habe viel Zeit damit verbracht, die Ursache zu untersuchen, also werde ich das Ergebnis geben. Ich habe es zusammengefasst. Als ich es mit der REST-Edition der Todo-Anwendung versuchte, wurde 200 zurückgegeben, aber wahrscheinlich funktionierte die unerwartete Einstellung (entsprechend).

Ich konnte bestätigen, dass das erwartete Verhalten erreicht wurde, indem der in web.xml beschriebene Filter auf MockMVC angewendet wurde, jedoch" Bei Verwendung von MockMVC, web.xml Ich war in Schwierigkeiten, weil ich das Dokument nicht finden konnte, obwohl es einen Kommentar gab, der besagte "Ich kann es nicht lesen" ...! Offizielles Dokument 15.6 Spring MVC Test Framework, Wenn Sie lesen

The Spring MVC Test framework provides first class support for testing Spring MVC code using a fluent API that can be used with JUnit, TestNG, or any other testing framework. It’s built on the Servlet API mock objects from the spring-test module and hence does not use a running Servlet container.

Es heißt, dass es auf dem Servlet-Container nicht funktioniert, daher scheint "web.xml" nichts mit "MockMVC" oder "Spring MVC Test" zu tun zu haben.

Recommended Posts

Eine Geschichte, der ich beim Testen der API mit MockMVC verfallen war
Die Geschichte, nach der ich beim Einrichten von STS süchtig war
Memorandum: Wovon ich süchtig war, als ich auf die Accounting Freee API traf
[Rails] Ich war süchtig nach den Nginx-Einstellungen, als ich Action Cable verwendete.
Wovon ich bei der Einführung der JNI-Bibliothek süchtig war
Was ich mit der Redmine REST API süchtig gemacht habe
Ich war seltsamerweise süchtig danach, Javas Stream-API mit Scala zu verwenden
Eine Geschichte, nach der ich süchtig war, als ich einen Schlüssel bekam, der automatisch auf MyBatis ausprobiert wurde
Ich war süchtig nach der Rollmethode
Ich war süchtig nach dem Spring-Batch-Test
[Circle CI] Eine Geschichte, der ich bei Start Building verfallen war
Ein Hinweis, als ich süchtig danach war, Ubuntu auf WSL1 in WSL2 zu konvertieren
Einfache Möglichkeit zum Erstellen einer Zuordnungsklasse bei Verwendung der API
Ich war süchtig nach der API-Version min23 von registerTorchCallback
Eine Geschichte, die mich während des Komponententests von der impliziten Typkonvertierung von ActiveRecord abhängig machte
Ich war süchtig nach RXTX mit Sierra
Probleme, denen ich beim Erstellen der Digdag-Umgebung mit Docker verfallen war
Ich war süchtig nach Unit-Tests mit dem Pufferoperator in RxJava
Eine Geschichte, der ich mit der automatischen Starteinstellung von Tomcat 8 unter CentOS 8 zweimal verfallen war
Ich war ein wenig süchtig nach dem S3-Prüfsummenvergleich, machen Sie sich also eine Notiz.
Ich war süchtig nach NoSuchMethodError in Cloud-Endpunkten
Ich war süchtig nach der Aufzeichnung des zugehörigen Modells
Als ich eine Methode für Premium Friday erstellen wollte, war sie bereits in der Java 8-Standard-API enthalten
Wovon ich süchtig war, als ich eine Spring Boot-Anwendung mit VS Code entwickelte
Beachten Sie, dass ich süchtig nach Stapelverarbeitung mit Spring Boot war
Bei der Erstellung einer persönlichen App habe ich mich gefragt, ob ich sie mit haml erstellen soll
Beachten Sie, dass ich von den Einstellungen des Android-Projekts von IntelliJ IDEA abhängig war
Was ich beim Update auf Spring Boot 1.5.12 behoben habe ・ Wovon ich süchtig war
Wovon ich süchtig war, als ich rspec auf Schienen benutzte
Ich war süchtig danach, die Update-Anweisung in MyBatis zu wiederholen
Ich war süchtig nach Laradock + VSCode + xdebug
Ich war süchtig danach, sbt zu starten
Wovon ich süchtig war, als ich die Google-Authentifizierung mit Rails implementierte
Über die Sache, dass ich süchtig danach war, wie man Hashmap benutzt
Eine Geschichte, die süchtig nach Platzhaltern für JDBC-Vorlagen ist
Ich war süchtig danach, in @ SpringApplicationConfiguration-> @SpringBootTest umzuschreiben
Ich habe versucht, die Stream-API zusammenzufassen
Ein Memo, das nüchtern von der Anfrage nach mehrteiligen / Formulardaten abhängig war
Ich habe die Punkte zusammengefasst, die bei der kombinierten Verwendung von Ressourcen und Ressourcen zu beachten sind
Ich war süchtig danach, MySQL 8.0 mit einem 5.7-Gefühl zu berühren. My.cnf-Konfigurationsproblem
Ich war süchtig nach einem einfachen Test von Jedis (Java-> Redis-Bibliothek)
Aufgenommen, weil ich süchtig nach der Standardeingabe der Scannerklasse war
Es war ein Leben, in dem ich den thread-sicheren assoziativen Zähler zurücksetzen wollte
Ich war süchtig nach Scrollview, weil ich nicht auf die UIView mit variabler Größe tippen konnte
[Java small story] Überwachen Sie, wann der Liste ein Wert hinzugefügt wird
[Circle CI] Ich war süchtig nach dem automatischen Test von Circle CI (Rails + MySQL) [Memo]
Ich war ein wenig süchtig nach SSH-Verbindung von Mac zu Linux (Ubuntu)
Eine Geschichte, die ich mit der Stream-API von Java8 einem Prozess schreiben wollte, der einer while-Anweisung entspricht
Eine Geschichte, die süchtig nach EntityNotFoundException von getOne of JpaRepository ist
Eine Geschichte, als ich versuchte, ein Video zu erstellen, indem ich Processing und Resolume verknüpfte
Was ich versucht habe, als ich alle Felder einer Bohne bekommen wollte
03. Ich habe eine Anfrage von Spring Boot an die Postleitzahlensuch-API gesendet
Ich war süchtig danach, onActivityResult () mit DialogFragment zu machen
Ich war süchtig danach, vom Docker-Container aus keine Verbindung zu AWS-S3 herstellen zu können