Question

I'm using a restful url to kick off a long-running backend process (it is normally on a cron schedule, but we want the ability to kick it off manually).

The code below works and I see the result in the browser when I test manually.

@ResponseBody
@RequestMapping(value = "/trigger/{jobName}", method = RequestMethod.GET)
public Callable<TriggerResult> triggerJob(@PathVariable final String jobName) {

    return new Callable<TriggerResult>() {
        @Override
        public TriggerResult call() throws Exception {
            // Code goes here to locate relevant job and kick it off, waiting for result
            String message = <result from my job>;
            return new TriggerResult(SUCCESS, message);
        }
    };
}

When I test this without the Callable I used the code below and it all works (I changed the expected error message to simplify post).

mockMvc.perform(get("/trigger/job/xyz"))
    .andExpect(status().isOk())
    .andDo(print())
    .andExpect(jsonPath("status").value("SUCCESS"))
    .andExpect(jsonPath("message").value("A meaningful message appears"));

When I added the Callable however it does not work. I also tried below but it did not work. Anyone else had success?

mockMvc.perform(get("/trigger/job/xyz"))
    .andExpect(status().isOk())
    .andDo(print())
    .andExpect(request().asyncResult(jsonPath("status").value("SUCCESS")))
    .andExpect(request().asyncResult(jsonPath("message").value("A meaningful message appears")));

Below is the relevant part from my print(). Looks like mockMvc can't untangle the Json correctly in this case (even though it works in my browser)? When I do this without Callable I see full JSON.

MockHttpServletRequest:
     HTTP Method = GET
     Request URI = /trigger/job/xyz
      Parameters = {}
         Headers = {}

         Handler:
            Type = foo.bar.web.controller.TriggerJobController
          Method = public java.util.concurrent.Callable<foo.bar.myproject.web.model.TriggerResult> foo.bar.myproject.web.controller.TriggerJobController.triggerJob(java.lang.String)

           Async:
 Was async started = true
      Async result = foo.bar.myproject.web.model.TriggerResult@67aa1e71


Resolved Exception:
            Type = null

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

        FlashMap:

MockHttpServletResponse:
          Status = 200
   Error message = null
         Headers = {}
    Content type = null
            Body = 
   Forwarded URL = null
  Redirected URL = null
         Cookies = []
Was it helpful?

Solution

Bud's answer really helped point me in the right direction however it didn't quite work because it did not wait for the async result. Since posting this question, the spring-mvc-showcase samples (https://github.com/SpringSource/spring-mvc-showcase) have been updated.

It seems like in the first part of the call when you retrieve the MvcResult, you need to assert on an asyncResult() and in the case of JSON pojo mapping you need to assert on the actual type itself (not JSON). So I needed to add the third line below to Bud's answer, then the rest just works.

MvcResult mvcResult = this.mockMvc.perform(get("/trigger/job/xyz"))
    .andExpect(request().asyncStarted())
    .andExpect(request().asyncResult(instanceOf(TriggerResult.class)))
    .andReturn();

this.mockMvc.perform(asyncDispatch(mvcResult))
    .andExpect(status().isOk())
    .andExpect(content().contentType(MediaType.APPLICATION_JSON))
    .andExpect(jsonPath("status").value("SUCCESS"))
    .andExpect(jsonPath("message").value("A meaningful message appears"));

Note: instanceOf() is org.hamcrest.CoreMatchers.instanceOf. To get access to Hamcrest libraries include the latest hamcrest-library jar.

For maven ...

    <dependency>
        <groupId>org.hamcrest</groupId>
        <artifactId>hamcrest-library</artifactId>
        <version>LATEST VERSION HERE</version>
        <scope>test</scope>
    </dependency>

OTHER TIPS

Matt's answer is correct, but I would like perform to just work. Below is a perform method that you can use to test both async and sync requests. So you don't need to care in your tests how backend handles the requests. You are only interested of the actual response anyway, right?

ResultActions perform(MockHttpServletRequestBuilder builder) throws Exception {
    ResultActions resultActions = mockMvc.perform(builder);
    if (resultActions.andReturn().getRequest().isAsyncStarted()) {
      return mockMvc.perform(asyncDispatch(resultActions
          .andExpect(request().asyncResult(anything()))
          .andReturn()));
    } else {
      return resultActions;
    }
}

One way to integrate that to your tests is to put it in a common abstract base class and extend your actual test classes from it:

import static org.hamcrest.Matchers.anything;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup;

@WebAppConfiguration
@ContextConfiguration("file:src/main/webapp/WEB-INF/spring/appServlet/servlet-context.xml")
public abstract class AbstractMockMvcTests {

  @Autowired
  protected WebApplicationContext wac;

  private MockMvc mockMvc;

  @Before
  public void setup() throws Exception {
    mockMvc = webAppContextSetup(this.wac).build();
  }

  protected ResultActions perform(MockHttpServletRequestBuilder builder) throws Exception {
    ResultActions resultActions = mockMvc.perform(builder);
    if (resultActions.andReturn().getRequest().isAsyncStarted()) {
      return mockMvc.perform(asyncDispatch(resultActions
          .andExpect(request().asyncResult(anything()))
          .andReturn()));
    } else {
      return resultActions;
    }
  }
}

Then implement your tests by extending the base class and use the perform method. In this example mockMvc is made private to gently guide all future test authors to use the custom perform method.

@RunWith(SpringJUnit4ClassRunner.class)
public class CallableControllerTests extends AbstractMockMvcTests {

  @Test
  public void responseBodyAsync() throws Exception {
    perform(get("/async/callable/response-body"))
      .andExpect(status().isOk())
      .andExpect(content().contentType("text/plain;charset=ISO-8859-1"))
      .andExpect(content().string("Callable result"));
  }

  @Test
  public void responseBodySync() throws Exception {
    perform(get("/sync/foobar/response-body"))
      .andExpect(status().isOk())
      .andExpect(content().contentType("text/plain;charset=ISO-8859-1"))
      .andExpect(content().string("Sync result"));
  }
}

I think you want to use asyncDispatch on the result of the started Async calls Reference code from link below

http://static.springsource.org/spring/docs/3.2.x/javadoc-api/org/springframework/test/web/servlet/request/MockMvcRequestBuilders.html

Usage involves performing one request first that starts async processing:

 MvcResult mvcResult = this.mockMvc.perform(get("/trigger/job/xyz"))
        .andExpect(request().asyncStarted())
        .andReturn();

And then performing the async dispatch re-using the MvcResult:

 this.mockMvc.perform(asyncDispatch(mvcResult))
        .andExpect(status().isOk())
        .andExpect(content().contentType(MediaType.APPLICATION_JSON))
        .andExpect(content().string(.......));

or in your case

this.mockMvc.perform(asyncDispatch(mvcResult))
        .andExpect(status().isOk())
        .andExpect(content().contentType(MediaType.APPLICATION_JSON))
        .andExpect(jsonPath("status").value("SUCCESS"))
        .andExpect(jsonPath("message").value("A meaningful message appears"));
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top