mat_boy
mat_boy

Reputation: 13666

Testing a call to a rest controller from controller in Spring

I'm using RestTemplate to call a rest controller from a controller, e.g.:

@RequestMapping(method = POST, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    @ResponseBody
    public String formForUpload(@RequestParam("file") MultipartFile file) throws Exception {
    final MultiValueMap<String, Object> parts=...;
    restTemplate.postForObject("http://localhost:8080/rest/something",
                        parts, MultipartFile.class);
...
}

How can I unit test that the URL called by the controller is correct. Is there any way to get the path for the rest controller (e.g. using such reflection technique)?

I don't want to run an integration test!

Upvotes: 3

Views: 6088

Answers (3)

JeanValjean
JeanValjean

Reputation: 17713

Maybe this is not the correct answer, but maybe it helps.

Assuming that the rest controller (let say MyRestController) has the class declaration annotated with @RequestMapping, then I assume that you can simply get the currently defined path as follows:

final String controllerPath = MyRestController.class
                    .getAnnotation(RequestMapping.class).value()[0];

UPDATE Why don't you try the following changes to your code. They are based on Spring, JUnit and Mockito. Assuming your controller is class MyController.

@Test
    public void testUploadPostCallsToRestService() throws Exception {
        final MockMultipartFile file = //create a mock for a file to be uploaded;

        final MyController myController = new MyController();

        final RestTemplate restTemplate = new RestTemplate();
        final MockRestServiceServer mockServer = MockRestServiceServer.createServer(restTemplate);

        ReflectionTestUtils.setField(myController, "restTemplate", restTemplate); //I assume you use Spring MVC from your tagging, isn't it?

        final MockMvc mockMvc = MockMvcBuilders.standaloneSetup(myController).build();
mockServer.expect(requestTo("http://localhost:8080/rest/something"))
                .andExpect(method(POST))
                .andRespond(withStatus(OK)); //Or whatever your rest returns

        mockMvc.perform(fileUpload("http://localhost:8080/path/to/my/controller")
                .file(file)
                .accept(MediaType.MULTIPART_FORM_DATA))
                .andExpect(status().isOk()); //I hope you return this :)

        mockServer.verify();
    }

This works very well for me. Essentially, I use the mock for the rest service server that has to serve the RestTemplate. So, the latter will call the mock that behaves as you specified. Then, it is injected in your controller using reflection. Finally, I use MockMvc to upload the file and get the answer from the controller.

Upvotes: 4

Tomas Pinos
Tomas Pinos

Reputation: 2862

Is method argument value capturing what you're looking for?

You can use a mocking framework to mock the rest template and capture the method invocation arguments.

I suggest to use TestNG and JMockit.

Controller class (the example is working code, so I want to show the whole controller class):

import org.springframework.beans.factory.annotation.*;
import org.springframework.http.MediaType;
import org.springframework.util.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;

public class MyController {

    @Autowired
    @Qualifier("MyRestTemplate")
    private RestTemplate restTemplate;

    @RequestMapping(method = RequestMethod.POST, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    @ResponseBody
    public String formForUpload(@RequestParam("file") MultipartFile file) {
        MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
        restTemplate.postForObject("http://localhost:8080/rest/something", parts, MultipartFile.class);
        return "[{\"good-data\"}]";
    }
}

The unit test:

import mockit.*;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
import org.testng.Assert;
import org.testng.annotations.Test;

import java.util.ArrayList;

public class MyControllerTest {

    @Test
    public void testFormForUpload(@Mocked final RestTemplate restTemplate) {
        MyController controller = new MyController();
        Deencapsulation.setField(controller, restTemplate);

        final ArrayList<String> urlValueHolder = new ArrayList<>();

        new Expectations() {{
            restTemplate.postForObject(withCapture(urlValueHolder), any, MultipartFile.class);
        }};

        controller.formForUpload(new MockMultipartFile("data-file.txt", "various data".getBytes()));

        Assert.assertEquals(urlValueHolder.size(), 1);
        Assert.assertEquals(urlValueHolder.get(0), "http://localhost:8080/rest/something");
    }
}

Notes:

  • The RestTemplate mock is passed as an argument of the test method. That's all you need to do to create a working mock.
  • See withCapture call that I use as the first argument of RestTemplate#postForObject. The url(s) used by the controller will be stored in the list urlValueHolder.
  • There're two assertions for the url list - that it was called once and that it contains the required url.

I believe the test is easy enough, but still I would recommend to refactor the controller. Instead of using mocks and testing controllers to verify the urls, I would suggest to move the url creation to a separate factory class. A simple unit test of the factory would be all that's needed to test the urls.

Upvotes: 1

Kafkaesque
Kafkaesque

Reputation: 1283

There is a class for generating links to controllers and controller methods in the Spring Hateoas project. Specifically have a look at org.springframework.hateoas.mvc.ControllerLinkBuilder. It will be able to generate URL's for simple @RequestMapping annotations, but if you use multiple mappings on the same handler method it won't be able to figure out which one to generate.

Also, if your controller handler methods return String, the link builder will throw an error, so change the return type to Object and it'll work.

Upvotes: 0

Related Questions