[JAVA] Une histoire à laquelle j'étais accro lors du test de l'API à l'aide de MockMVC

Environnement de développement

Contexte

J'ai décidé de tester RestController, mais comme il est difficile de déployer l'application à chaque fois que je teste, j'ai décidé de tester en utilisant Mock. Quand il s'agit d'utiliser Mock pour tester une application développée avec Spring, il existe plusieurs méthodes et bibliothèques, mais cette fois nous utiliserons MockMVC.

Qu'est-ce que MockMVC

L'une des fonctionnalités fournies par Spring Test, vous pouvez reproduire le comportement de Spring MVC sans déployer l'application sur le serveur. Il y a deux façons d'utiliser MockMVC, l'une consiste à utiliser le conteneur DI pour les applications Web ( WebApplicationContext), et l'autre consiste à utiliser le conteneur DI généré par Spring Test. Cette fois, nous utiliserons l'ancien: grinning:.

Pour plus de détails, veuillez vous référer aux directives TERA SOLUNA. Comment utiliser la bibliothèque OSS utilisée pour le test unitaire -Qu'est-ce que MockMvc-

Réalisation du test

Code à tester

Prenons comme exemple l'application Todo de TERASOLUNA (édition REST). L'API à tester est «post Todos» qui crée Todo. Si vous définissez Title sur l'objet TodoResource et post au format JSON, l'id et la date seront donnés et le TodoResource incomplet sera retourné.

Les packages, les importations, les getters et les setters sont omis. Le code testé n'est pas important dans cet article, je vais donc omettre des choses comme Todo.java et TodoService.java.

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>

    <!--La définition des différents filtres est omise-->

    <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>
Implémentation du code de test

Confirmez que l'objet TodoResource généré est renvoyé.

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());
  }
}
Exécuter le code de test

Quand je l'exécute, le code d'état est OK (200) au lieu de Créé (201) ...: fatigué:

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)

À propos, si vous déployez réellement sur le serveur et utilisez Postman pour POST vers / api / v1 / todos, Todo sera créé et 201 sera retourné.

postmanでの結果

Après cela, j'ai soigneusement vérifié qu'il n'y avait pas d'erreurs dans l'URL et les paramètres, mais seulement le temps passait ...

Solution

Examiner TestDispatcherServlet

Lisez Comment utiliser la bibliothèque OSS utilisée pour le test unitaire - Qu'est-ce que MockMvc- Comme je l'ai remarqué, «MockMVC» semble utiliser «TestDispatcherServlet» pour répondre à une pseudo requête. Regardons en fait TestDispatcherServlet.java ici. 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);
	}

Vous appelez le constructeur de la classe parent DispatcherServlet dans le constructeur. Ensuite, jetons un œil à DispatcherServlet.java.

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:
	 * ~Abréviation~
	 */
	public DispatcherServlet(WebApplicationContext webApplicationContext) {
		super(webApplicationContext);
		setDispatchOptionsRequest(true);
	}

Il dit "Créer un" DispatcherServlet "dans le contexte d'application spécifié." Regardons également le constructeur sans argument au-dessus de ce code

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.
	 * ~Abréviation~
	 */
	public DispatcherServlet(WebApplicationContext webApplicationContext) {
		super(webApplicationContext);
		setDispatchOptionsRequest(true);
	}

À côté de "Créer un nouveau" Servlet de diffusion "qui fournit votre propre contexte d'application Web interne.", Ensuite "La seule façon d'enregistrer un servlet est" web.xml ", qui nécessite l'utilisation d'un constructeur sans argument. "(Traduction gratuite). En d'autres termes, le TestDispatcherServlet utilisé par MockMVC appelle le constructeur avec l'argument de DispatcherServlet, donc le contenu de la description de web.xml n'est pas reflété. Cela signifie t-il? En recherchant des mots qui semblent pertinents pour l'essai, la version de Spring Framework est 3.2, mais certaines personnes demandaient comment charger web.xml dans un test utilisant MockMvc. ..

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

Les répondants disent que "Spring-mvc-test ne lit pas le fichier web.xml".

Correctif de test

Si le contenu décrit dans web.xml n'est pas reflété, cela signifie que servlet-mapping ne fonctionne pas, donc le chemin réel sera / todos au lieu de / api / v1 / todos. .. Alors corrigeons le code de test et réessayons.

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

Je l'ai fait!

Je veux refléter le contenu de web.xml dans le code de test ...

C'était bien que cela fonctionne avec ↑, mais cela semble un peu désagréable que le chemin réel de l'application soit / api / v1 / todos et que le chemin du test soit / todos. Je sais que les filtres peuvent être ajoutés avec ʻaddFilter` comme suit: sos:.

TodoRestControllerTest.java


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

Contexte de l'affichage

Le code décrit dans cet article est tiré de l'exemple, mais dans le développement actuel, la spécification de ʻURL` devrait être correcte, mais elle est devenue 404, et j'ai passé beaucoup de temps à rechercher la cause, donc je vais donner le résultat. Je l'ai résumé. Lorsque je l'ai essayé avec l'édition REST de l'application Todo, 200 a été renvoyé, mais le paramètre inattendu fonctionnait probablement (approprié).

En appliquant le filtre etc. décrit dans web.xml à MockMVC, il a été confirmé que le comportement attendu était atteint, mais" Lors de l'utilisation de MockMVC, web.xml J'avais des ennuis parce que je n'ai pas trouvé le document même s'il y avait un commentaire disant "Je ne peux pas le lire" ...! Document officiel 15.6 Spring MVC Test Framework, Si vous lisez

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.

Il dit que cela ne fonctionne pas sur le conteneur de servlet, donc il semble que web.xml n'a rien à voir avec MockMVC ou Spring MVC Test.

Recommended Posts

Une histoire à laquelle j'étais accro lors du test de l'API à l'aide de MockMVC
L'histoire à laquelle j'étais accro lors de la création de STS
Mémorandum: Ce à quoi j'étais accro quand j'ai frappé l'API de comptabilité freee
[Rails] J'étais accro aux paramètres nginx lors de l'utilisation d'Action Cable.
Ce à quoi j'étais accro lors de l'introduction de la bibliothèque JNI
Ce à quoi j'étais accro avec l'API REST Redmine
J'étais étrangement accro à l'utilisation de l'API Stream de Java avec Scala
Une histoire à laquelle j'étais accro lors de l'obtention d'une clé qui a été automatiquement essayée sur MyBatis
J'étais accro à la méthode du rouleau
J'étais accro au test Spring-Batch
[Circle CI] Une histoire à laquelle j'étais accro chez Start Building
Une note quand j'étais accro à la conversion d'Ubuntu sur WSL1 en WSL2
Un moyen simple de créer une classe de mappage lors de l'utilisation de l'API
J'étais accro au paramètre API version min23 de registerTorchCallback
Une histoire dans laquelle j'étais accro à la conversion de type implicite d'ActiveRecord lors du test unitaire
J'étais accro à l'utilisation de RXTX avec Sierra
Problèmes auxquels j'étais accro lors de la création de l'environnement digdag avec docker
J'étais accro aux tests unitaires avec l'opérateur de tampon dans RxJava
Une histoire à laquelle j'étais accro à deux reprises avec le paramètre de démarrage automatique de Tomcat 8 sur CentOS 8
J'étais un peu accro à la comparaison S3 Checksum, alors prenez note.
J'étais accro à NoSuchMethodError dans Cloud Endpoints
J'étais accro au record du modèle associé
Quand j'ai voulu créer une méthode pour Premium Friday, c'était déjà dans l'API standard Java 8
Ce à quoi j'étais accro lors du développement d'une application Spring Boot avec VS Code
Notez que j'étais accro au traitement par lots avec Spring Boot
Lors de la création d'une application personnelle, je me demandais si je devais la faire en utilisant haml
Notez que j'étais accro aux paramètres du projet Android d'IntelliJ IDEA
Ce que j'ai corrigé lors de la mise à jour vers Spring Boot 1.5.12 ・ Ce à quoi j'étais accro
Ce à quoi j'étais accro en utilisant rspec sur des rails
J'étais accro à la mise à jour de la déclaration dans MyBatis
J'étais accro au réglage de laradock + VSCode + xdebug
J'étais accro au démarrage de sbt
L'histoire que je voulais développer Zip
Ce à quoi j'étais accro lors de la mise en œuvre de l'authentification Google avec des rails
À propos de la question pour laquelle j'étais accro à l'utilisation de hashmap
Une histoire accro aux espaces réservés des modèles JDBC
J'étais accro à la réécriture sur @ SpringApplicationConfiguration-> @SpringBootTest
J'ai essayé de résumer l'API Stream
Un mémo sobrement accro à la demande de multipart / form-data
J'ai résumé les points à noter lors de l'utilisation combinée des ressources et des ressources
J'étais accro à l'idée de toucher MySQL 8.0 avec un sentiment de 5.7. Problème de configuration My.cnf
J'étais accro à un simple test de Jedis (bibliothèque Java-> Redis)
Enregistré parce que j'étais accro à l'entrée standard de la classe Scanner
C'était une vie que je voulais réinitialiser le compteur associatif thread-safe
J'étais accro au scrollview car je ne pouvais pas appuyer sur la taille variable UIView
[Petite histoire Java] Surveiller lorsqu'une valeur est ajoutée à la liste
[Circle CI] J'étais accro au test automatique de Circle CI (rails + mysql) [Memo]
J'étais un peu accro à la connexion ssh de mac à linux (ubuntu)
Je voulais écrire un processus équivalent à une instruction while avec l'API Java 8 Stream
Une histoire accro à EntityNotFoundException de getOne de JpaRepository
Une histoire où j'ai essayé de faire une vidéo en liant Traitement et Resolume
Ce que j'ai essayé quand je voulais obtenir tous les champs d'un haricot
03. J'ai envoyé une demande de Spring Boot à l'API de recherche de code postal
J'étais accro à faire onActivityResult () avec DialogFragment
J'étais accro à ne pas pouvoir me connecter à AWS-S3 à partir du conteneur Docker