No JSON content (comes up null) from MockMvc Test

2019-08-02 13:47发布

问题:

Have a codebase which uses SpringMVC 4.0.3.RELEASE for Restful Web Services.

Codebase contains a fully functional Restful Web Service which I can verify works using postman and curl.

However, when trying to write a unit test for the particular Restful Web Service using MockMvc, I become blocked with trying to obtain the JSON content from the unit test.

Am wondering if its a config issue or an issue where I am not creating a fake object correctly (since this doesn't rely on tomcat and is standalone).

pom.xml:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>4.0.3.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>4.0.3.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>4.0.3.RELEASE</version>
    <scope>test</scope>
</dependency>

<!-- A bunch of other Spring libs omitted from this post -->

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.1.0</version>
    <scope>provided</scope>
</dependency>

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<dependency>
    <groupId>org.hamcrest</groupId>
    <artifactId>hamcrest-all</artifactId>
    <version>1.3</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>1.10.19</version>
    <exclusions>
        <exclusion>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest-core</artifactId>
        </exclusion>
    </exclusions>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>com.jayway.jsonpath</groupId>
    <artifactId>json-path-assert</artifactId>
    <version>0.9.1</version>
    <scope>test</scope>
</dependency>

WebConfig:

package com.myapp.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@EnableWebMvc
@Configuration
@ComponentScan("com.myapp.rest")
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.defaultContentType(MediaType.APPLICATION_JSON);
    }
}

ServletInitializer:

package com.myapp.config;

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

import com.myapp.config.WebConfig;

public class ServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    protected Class<?>[] getServletConfigClasses() {
        return new Class[] { WebConfig.class };
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/" };
    }

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return null;
    }
}

UserController:

@RestController
@RequestMapping("/v2")
public class UserController {

    private final UserDAO dao;

    private HttpHeaders headers = null;

    @Autowired 
    public UserController(UserDAO dao) {
        this.dao = dao;
        headers = new HttpHeaders();
        headers.add("Content-Type", "application/json");
    }

    @RequestMapping(value = "users/{appId}", method = RequestMethod.GET, produces="application/json")
    @ResponseBody 
    public ResponseEntity<Object> getUserDetails(@PathVariable String appId) {
        Object jsonPayload = dao.getUser(appId);
        return new ResponseEntity<Object>(jsonPayload, headers, HttpStatus.OK);
    }
}

UserDAO:

@Repository
public class UserDAO {

    private JdbcTemplate jdbcTemplate;

    @Autowired
    public UserDAO(@Qualifier("dataSourceDB") DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    // Finders methods (such as the getUser(appId)) which use Spring JDBC
}

WEB-INF/web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4"
    xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">

    <display-name>MyApp</display-name>

    <welcome-file-list>
        <welcome-file>index.html</welcome-file>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>

    <servlet>
        <servlet-name>mvc-dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>mvc-dispatcher</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/mvc-dispatcher-servlet.xml</param-value>
    </context-param>
</web-app>

WEB-INF/mvc-dispatcher-servlet.xml:

<beans xmlns="http://www.springframework.org/schema/beans">

    <import resource="classpath:database.xml" />
    <context:component-scan base-package="com.myapp.rest" />
    <mvc:annotation-driven />

    <bean id="viewResolver"
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix">
            <value>/WEB-INF/pages/</value>
        </property>
        <property name="suffix">
            <value>.jsp</value>
        </property>
    </bean>
</beans>

src/main/resources/database.xml:

<beans xmlns="http://www.springframework.org/schema/beans">
    <bean id="dataSourceDB" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName"><value>${jdbc.driver}</value></property>
        <property name="url"><value>${db.url}</value></property>
        <property name="username"><value>${db.username}</value></property>
        <property name="password"><value>${db.password}</value></property>
    </bean>
</beans>

My actual unit test: package com.myapp.rest.controllers;

RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:**/mvc-dispatcher-servlet.xml")
@WebAppConfiguration
public class UserControllerTest {

    private MockMvc mockMvc;

    @Mock
    private UserDAO dao;

    @InjectMocks
    private UserController controller;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
    }

    @Test
    public void getUserDetails() throws Exception {
        String appId = "23bdr4560l";
        mockMvc.perform(get("/v2/users/{appId}",appId)
               .accept(MediaType.APPLICATION_JSON))
               .andExpect(content().contentType(MediaType.APPLICATION_JSON))
               .andExpect(status().isOk())
               .andDo(print());
    }
}

When I invoke my build locally:

mvn clean install

Generated output to stdout:

Running com.myapp.rest.controllers.UserControllerTest

MockHttpServletRequest:
         HTTP Method = GET
         Request URI = /v2/users/23bdr4560l
          Parameters = {}
             Headers = {Accept=[application/json]}

             Handler:
                Type = com.myapp.rest.controllers.UserController
              Method = public org.springframework.http.ResponseEntity<java.lang.Object> com.myapp.rest.controllers.UserController.getUserDetails(java.lang.String)

               Async:
   Was async started = false
        Async result = null

  Resolved Exception:
                Type = null

        ModelAndView:
           View name = null
                View = null
               Model = null

            FlashMap:

MockHttpServletResponse:
              Status = 200
       Error message = null
             Headers = {Content-Type=[application/json]}
        Content type = application/json
                Body = 
       Forwarded URL = null
      Redirected URL = null
             Cookies = []
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.728 sec

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

Notice how the JSON body part is empty / null:

Body = 

So, when trying to access the JSON payload using jsonPath:

@Test
public void getUserDetails() throws Exception {
    String appId = "23bdr4560l";
    mockMvc.perform(get("/v2/users/{appId}",appId)
           .accept(MediaType.APPLICATION_JSON))
           .andExpect(content().contentType(MediaType.APPLICATION_JSON))
           .andExpect(status().isOk())
           .andExpect(jsonPath("$", hasSize(2)))
           .andDo(print());
}

Received the following error:

Results :

Tests in error: 
  getUserDetails(com.myapp.rest.controllers.UserControllerTest): json can not be null or empty

Tests run: 1, Failures: 0, Errors: 1, Skipped: 0

What am I possbily doing wrong?

Obviously, I need to mock out the response but have Googled this and others using MockMvc were able to obtain a JSON their test.

Is this a config issue (do I need to put some type of annotation for my unit test)?

Do, I need to instantiate the controller inside my test?

What weird is that the actual Rest Call works (returns a valid JSON) using postman and curl...

This codebase does not have an @Service class / layer, its just @RestController speaking to @Repository (see above).

Really thought that testing support for Spring MVC based Restful Web Services would be a lot easier.

Am running out of ideas...

Any help would me most appreciated...

回答1:

When you are trying to mock the bean like this, it tries to create the mock with the default constructor.

@Mock
private UserDAO dao

It works in case of actual rest call because you provide the dependent DataSource dataSource at runtime. Providing a valid dependent mock for the UserDAO class works.



回答2:

Since you are mocking the UserDAO you need to provide some mock expectations for the call:

Object jsonPayload = dao.getUser(appId);

Inside your Controller or it will return null by default. To do this use the Mockito.when() method inside your test prior to the MockMvc call:

when(dao.getUser(anyString())).thenReturn(json)

where json is some hard coded json Object you set up yourself.



回答3:

For starters, since you're using MockMvcBuilders.standaloneSetup(...) you are in fact not using the Spring TestContext Framework (TCF) to load your WebApplicationContext. So just completely delete the following declarations in your test class:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:**/mvc-dispatcher-servlet.xml")
@WebAppConfiguration

Also, you can completely ignore your XML configuration files since they are not used without a WebApplicationContext.

As an aside, web.xml and Servlet initializers are never used by the TCF, so you can ignore them as well.

Then, as mentioned by @Plog, you'll need to set up the expectations for your mocked UserDAO; otherwise, the invocation of dao.getUser(appId) always returns null which results in an empty response body.

Regards,

Sam (author of the Spring TestContext Framework)