[JAVA] A story I was addicted to when testing the API using MockMVC

Development environment

background

I decided to test the RestController, but deploying the application every time I tested it was a hassle, so I decided to test it using the Mock. When it comes to using Mock to test an application developed with Spring, there are several methods and libraries, but this time we will use MockMVC.

What is MockMVC

One of the features provided by Spring Test, you can reproduce the behavior of Spring MVC without deploying the application on the server. There are two ways to use MockMVC, one is to use a DI container for web applications ( WebApplicationContext), and the other is to use a DI container generated by Spring Test. This time, we will use the former: grinning :.

For details, please refer to the TERA SOLUNA guidelines. How to use OSS library used for unit test -What is MockMvc-

Conducting the test

Code to be tested

Let's take TERASOLUNA's Todo application (REST version) as an example. The API to test is post Todos which creates Todo. If you set Title to TodoResource object and post in JSON format, id and date will be given and the incomplete TodoResource will be returned.

Packages, imports, getters & setters are omitted. The code under test is not important in this article, so I'll omit things like Todo.java and 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>

    <!--Definition of various filters is omitted-->

    <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>
Test code implementation

Confirm that the generated TodoResource object is returned.

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());
  }
}
Run test code

When I run it, the status code is OK (200) instead of Created (201) ...: weary:

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)

By the way, if you actually deploy to the server and use Postman to POST to / api / v1 / todos, Todo will be created and 201 will be returned.

postmanでの結果

After this, I carefully checked that there were no mistakes in the ʻURL` and settings, but only time passed ...

Solution

Examine TestDispatcherServlet

Read How to use OSS library used for unit test-What is MockMvc- I noticed that MockMVC seems to use TestDispatcherServlet to fulfill a pseudo request. Let's actually look at TestDispatcherServlet.java here. 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);
	}

You are calling the constructor of the parent class DispatcherServlet in the constructor. Next, let's take a look at 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:
	 * ~Abbreviation~
	 */
	public DispatcherServlet(WebApplicationContext webApplicationContext) {
		super(webApplicationContext);
		setDispatchOptionsRequest(true);
	}

It says "Create aDispatcherServlet in the specified application context. " Let's also look at the no-argument constructor above this 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.
	 * ~Abbreviation~
	 */
	public DispatcherServlet(WebApplicationContext webApplicationContext) {
		super(webApplicationContext);
		setDispatchOptionsRequest(true);
	}

After "Create a newDispatcherServlet that provides your own internal web application context. ", Then" The only way to register a Servlet is with web.xml, which requires the use of a no-argument constructor. "(Free translation). …… In other words, TestDispatcherServlet used by MockMVC calls the constructor with arguments of DispatcherServlet, so the description contents of web.xml are not reflected. Does that mean? Looking up words that seem to be relevant to the trial, the version of Spring Framework is 3.2, but some people were asking about how to load web.xml in a test using MockMvc. ..

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

Respondents say "Spring-mvc-test does not read the web.xml file".

Test fix

If the contents described in web.xml are not reflected, it means that servlet-mapping is not working, so the actual path will be / todos instead of / api / v1 / todos. .. So let's fix the test code and try again.

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

I did it!

I want to reflect the contents of web.xml in the test code ...

It was good that it worked with ↑, but it is a little unpleasant that the actual application path is / api / v1 / todos and the test path is / todos. I know that filters can be added with ʻaddFilter` as follows: sos :.

TodoRestControllerTest.java


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

Background to the posting

The code described in this article is quoted from the sample, but in the actual development, the specification of ʻURL` should be correct, but it became 404, and I spent a lot of time investigating the cause, so I will give the result. I summarized it. When I tried it with the Todo application REST edition, 200 was returned, but probably the unexpected setting was working (appropriate).

By applying the filter etc. described in web.xml to MockMVC, it was confirmed that the expected behavior was achieved, but "When using MockMVC, web.xml I was in trouble because I couldn't find the document even though there was a comment stating "I can't read it" ...! Official documentation 15.6 Spring MVC Test Framework, If you read

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.

It says that it doesn't work on the Servlet container, so it seems that web.xml has nothing to do with MockMVC or Spring MVC Test.

Recommended Posts

A story I was addicted to when testing the API using MockMVC
The story I was addicted to when setting up STS
Memorandum: What I was addicted to when I hit the accounting freee API
[Rails] I was addicted to the nginx settings when using Action Cable.
What I was addicted to when introducing the JNI library
A story I was addicted to in Rails validation settings
What I was addicted to with the Redmine REST API
I was addicted to using Java's Stream API in Scala
A story I was addicted to when getting a key that was automatically tried on MyBatis
I was addicted to the roll method
I was addicted to the Spring-Batch test
[Circle CI] A story I was addicted to at Start Building
A note when I was addicted to converting Ubuntu on WSL1 to WSL2
Easy way to create a mapping class when using the API
I was addicted to the API version min23 setting of registerTorchCallback
A story I was addicted to before building a Ruby and Rails environment using Ubuntu (20.04.1 LTS)
A story I was addicted to with implicit type conversion of ActiveRecord during unit testing
I was addicted to using RXTX on Sierra
Problems I was addicted to when building the digdag environment with docker
I was addicted to unit testing with the buffer operator in RxJava
A story that I was addicted to twice with the automatic startup setting of Tomcat 8 on CentOS 8
I was a little addicted to the S3 Checksum comparison, so I made a note.
I was addicted to the NoSuchMethodError in Cloud Endpoints
I was addicted to the record of the associated model
When I wanted to create a method for Premium Friday, it was already in the Java 8 standard API
What I was addicted to when developing a Spring Boot application with VS Code
A memo that I was addicted to when making batch processing with Spring Boot
When making a personal app, I was wondering whether to make it using haml
A memorandum because I was addicted to the setting of the Android project of IntelliJ IDEA
What I fixed when updating to Spring Boot 1.5.12 ・ What I was addicted to
What I was addicted to while using rspec on rails
I was addicted to looping the Update statement on MyBatis
I was addicted to the setting of laradock + VSCode + xdebug
I was addicted to starting sbt
The story I wanted to unzip
What I was addicted to when implementing google authentication with rails
About the matter that I was addicted to how to use hashmap
What I was addicted to when updating the PHP version of the development environment (Docker) from 7.2.11 to 7.4.x
A story addicted to JDBC Template placeholders
I was addicted to rewriting to @SpringApplicationConfiguration-> @SpringBootTest
I tried to summarize the Stream API
A memo that was soberly addicted to the request of multipart / form-data
I summarized the points to note when using resources and resources in combination
My.cnf configuration problem that I was addicted to when I was touching MySQL 8.0 like 5.7
I was addicted to a simple test of Jedis (Java-> Redis library)
Recorded because I was addicted to the standard input of the Scanner class
I was a little addicted to running old Ruby environment and old Rails
It was a life I wanted to reset the thread-safe associative counter
I was addicted to scrollview because I couldn't tap the variable size UIView
[Java small story] Monitor when a value is added to the List
[CircleCI] I was addicted to the automatic test of CircleCI (rails + mysql) [Memo]
I was a little addicted to ssh connection from mac to linux (ubuntu)
A story that was embarrassing to give anison file to the production environment
A story that I wanted to write a process equivalent to a while statement with the Stream API of Java8
A story addicted to EntityNotFoundException of getOne of JpaRepository
A story when I tried to make a video by linking Processing and Resolume
What I tried when I wanted to get all the fields of a bean
03. I sent a request from Spring Boot to the zip code search API
I was addicted to installing Ruby/Tk on MacOS
I was addicted to doing onActivityResult () with DialogFragment
I was addicted to not being able to connect to AWS-S3 from the Docker container