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
.
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-
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
andTodoService.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>
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());
}
}
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.
After this, I carefully checked that there were no mistakes in the ʻURL` and settings, but only time passed ...
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
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
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".
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();
I did it!
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();
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