【问题标题】:Inject mock into Spring MockMvc WebApplicationContext将 mock 注入 Spring MockMvc WebApplicationContext
【发布时间】:2023-03-12 05:39:01
【问题描述】:

我正在使用 Spring-boot 测试(通过 JUnit4 和 Spring MockMvc)一个 REST 服务适配器。适配器只是将向它发出的请求传递给另一个 REST 服务(使用自定义 RestTemplate)并将附加数据附加到响应中。

我想运行 MockMvc 测试来执行控制器集成测试,但想用模拟覆盖控制器中的 RestTemplate 以允许我预定义第 3 方 REST 响应并防止它在每次测试。我已经能够通过实例化MockMvcBuilders.standAloneSetup() 并将其传递给控制器​​以使用post 中列出的注入的模拟进行测试(以及下面的设置)来完成此操作,但是我无法使用MockMvcBuilders.webAppContextSetup().

我浏览过其他几篇文章,但没有一篇回答关于如何实现这一点的问题。我想在测试中使用实际的 Spring 应用程序上下文而不是独立的,以防止在应用程序可能增长时出现任何空白。

编辑:我使用 Mockito 作为我的模拟框架,并试图将其中一个模拟注入到上下文中。如果这不是必需的,那就更好了。

控制器:

@RestController
@RequestMapping(Constants.REQUEST_MAPPING_PATH)
public class Controller{

    @Autowired
    private DataProvider dp;    

    @Autowired
    private RestTemplate template;

    @RequestMapping(value = Constants.REQUEST_MAPPING_RESOURCE, method = RequestMethod.GET)
    public Response getResponse(
            @RequestParam(required = true) String data,
            @RequestParam(required = false, defaultValue = "80") String minScore
            ) throws Exception {

        Response resp = new Response();

        // Set the request params from the client request
        Map<String, String> parameters = new HashMap<String, String>();
        parameters.put(Constants.PARAM_DATA, data);
        parameters.put(Constants.PARAM_FORMAT, Constants.PARAMS_FORMAT.JSON);

        resp = template.getForObject(Constants.RESTDATAPROVIDER_URL, Response.class, parameters);

        if(resp.getError() == null){
            resp.filterScoreLessThan(new BigDecimal(minScore));
            new DataHandler(dp).populateData(resp.getData());
        }
        return resp;
    }
}

测试类:

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@SpringApplicationConfiguration(classes = MainSpringBootAdapter.class)
@TestPropertySource("/application-junit.properties")
public class WacControllerTest {

    private static String controllerURL = Constants.REQUEST_MAPPING_PATH + Constants.REQUEST_MAPPING_RESOURCE + compressedParams_all;
    private static String compressedParams_all = "?data={data}&minScore={minScore}";

    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;

    @InjectMocks
    private Controller Controller;

    @Mock
    private RestTemplate rt;

    @Value("${file}")
    private String file;

    @Spy
    private DataProvider dp;

    @Before
    public void setup() throws Exception {
        dp = new DataProvider(file);    
        MockitoAnnotations.initMocks(this);
        this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
    }

    @Test
    public void testGetResponse() throws Exception {

        String[] strings = {"requestData", "100"};

        Mockito.when(
            rt.getForObject(Mockito.<String> any(), Mockito.<Class<Object>> any(), Mockito.<Map<String, ?>> any()))
            .thenReturn(populateTestResponse());

        mockMvc.perform(get(controllerURL, strings)
            .accept(Constants.APPLICATION_JSON_UTF8))
            .andDo(MockMvcResultHandlers.print());

        Mockito.verify(rt, Mockito.times(1)).getForObject(Mockito.<String> any(), Mockito.<Class<?>> any(), Mockito.<Map<String, ?>> any());

        }


        private Response populateTestResponse() {
            Response  resp = new Response();

            resp.setScore(new BigDecimal(100));
            resp.setData("Some Data");

            return resp;
    }
}

【问题讨论】:

    标签: java spring-boot mockito integration-testing junit4


    【解决方案1】:

    Spring 的MockRestServiceServer 正是您要找的。

    类的javadoc的简短描述:

    客户端 REST 测试的主要入口点。用于涉及直接或间接(通过客户端代码)使用 RestTemplate 的测试。提供了一种对将通过 RestTemplate 执行的请求设置细粒度期望的方法,以及一种定义要发回的响应的方法,从而无需实际运行的服务器。

    尝试像这样设置您的测试:

    @WebAppConfiguration
    @ContextConfiguration(classes = {YourSpringConfig.class})
    @RunWith(SpringJUnit4ClassRunner.class)
    public class ExampleResourceTest {
    
        private MockMvc mockMvc;
        private MockRestServiceServer mockRestServiceServer;
    
        @Autowired
        private WebApplicationContext wac;
    
        @Autowired
        private RestOperations restOperations;
    
        @Before
        public void setUp() throws Exception {
            mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
            mockRestServiceServer = MockRestServiceServer.createServer((RestTemplate) restOperations);
        }
    
    
        @Test
        public void testMyApiCall() throws Exception {
            // Following line verifies that our code behind /api/my/endpoint made a REST PUT
            // with expected parameters to remote service successfully
            expectRestCallSuccess();
    
            this.mockMvc.perform(MockMvcRequestBuilders.get("/api/my/endpoint"))
                .andExpect(status().isOk());
        }
    
        private void expectRestCallSuccess() {
            mockRestServiceServer.expect(
                requestTo("http://remote.rest.service/api/resource"))
                .andExpect(method(PUT))
                .andRespond(withSuccess("{\"message\": \"hello\"}", APPLICATION_JSON));
        }
    
    
    }
    

    【讨论】:

    • 在看到其他帖子后,我实际上已经尝试过使用它,但没有任何运气。在经历了很多挫折之后,我终于今天能够让它工作,并且没有意识到包含覆盖我的RestTemplate bean 的其他 @Configuration 也覆盖了 MockRestServiceServer 注入的 bean。我将提出另一种方法来说明如何在没有MockRestServiceServer 的情况下完成同样的事情。非常感谢。
    【解决方案2】:

    这是另一个解决方案。简单地说,它只是创建一个新的RestTemplate bean 并覆盖已经注册的那个。

    因此,虽然它执行的功能与 @mzc 答案相同,但它允许我使用 Mockito 更轻松地制作响应和验证匹配器。

    这不仅仅是几行代码,但它也避免了必须添加额外的代码来将 Response 对象转换为上述 mockRestServiceServer.expect().andRespond(&lt;String&gt;) 方法的 arg 的字符串。

    @RunWith(SpringJUnit4ClassRunner.class)
    @WebAppConfiguration
    @SpringApplicationConfiguration(classes = MainSpringBootAdapter.class)
    @TestPropertySource("/application-junit.properties")
    public class WacControllerTest {
    
        private static String Controller_URL = Constants.REQUEST_MAPPING_PATH + Constants.REQUEST_MAPPING_RESOURCE + compressedParams_all;
    
        @Configuration
            static class Config {
                @Bean
                @Primary
                public RestTemplate restTemplateMock() {
                    return Mockito.mock(RestTemplate.class);
            }
        }
    
        @Autowired
        private WebApplicationContext wac;
    
        private MockMvc mockMvc;
    
        @InjectMocks
        private Controller Controller;
    
        @Mock
        private RestTemplate rt;
    
        @Value("${file}")
        private String file;
    
        @Spy
        private DataProvider dp;
    
        @Before
        public void setup() throws Exception {
            dp = new DataProvider(file); 
    
            MockitoAnnotations.initMocks(this);
            this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
            this.rt = (RestTemplate) this.wac.getBean("restTemplateMock");
        }
    
        @Test
        public void testGetResponse() throws Exception {
    
            String[] strings = {"request", "100"};
    
            //Set the request params from the client request
            Map<String, String> parameters = new HashMap<String, String>();
            parameters.put(Constants.PARAM_SINGLELINE, strings[0]);
            parameters.put(Constants.PARAM_FORMAT, Constants.PARAMS_FORMAT.JSON);
    
            Mockito.when(
                rt.getForObject(Mockito.<String> any(), Mockito.<Class<Object>> any(), Mockito.<Map<String, ?>> any()))
                .thenReturn(populateTestResponse());
    
            mockMvc.perform(get(Controller_URL, strings)
                .accept(Constants.APPLICATION_JSON_UTF8))
                .andDo(MockMvcResultHandlers.print());
    
            Mockito.verify(rt, Mockito.times(1)).getForObject(Mockito.<String> any(), Mockito.<Class<?>> any(), Mockito.<Map<String, ?>> any());
    
            }
    
    
            private Response populateTestResponse() {
                Response  resp = new Response();
    
                resp.setScore(new BigDecimal(100));
                resp.setData("Some Data");
    
                return resp;
        }
    }
    

    【讨论】:

    • 我向不应该被命名的对手发誓(!),春天充满了谜团。这就像一个魅力。我几乎要开始实施配置文件了,还有什么不是的,但它做到了!谢谢。我的问题:为什么会这样?我尝试在另一个 \@Configuration 类中创建一个新的 \@Bean,但 Spring 抱怨有重复的 bean 和所有内容。是什么让它起作用?
    • @apil.tamang @Configuration 是 bean 的 java 配置。它与 xml 配置同义。在这种情况下,我们正在连接另一个 RestTemplate bean,但返回它的模拟实例。此外,@Primary“表示当多个候选者有资格自动装配单值依赖项时,应优先考虑一个 bean”
    • 好的。在我尝试过之前只是一个快速的。如果没有@Primary 注解,这个方案可以工作吗?除了您已经说过的以外,@Primary 有什么特别之处?
    • @apil.tamang 如果我没记错的话,将使用 Spring 的产品 RestTemplate。不是你需要的模拟。您可能需要稍后查看@BeanMock,以确保在测试之外扫描应用程序组件时不会发现此新 bean
    • @ethesx 谢谢,你让我开心。我认为生活中有比spring创建和组合测试上下文的方式更简单的事情:)
    【解决方案3】:

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-07-28
      • 2014-06-28
      • 2019-08-05
      • 1970-01-01
      • 2017-01-05
      • 1970-01-01
      相关资源
      最近更新 更多