集成测试将整个对象发布到 Spring MVC 控制器

Posted

技术标签:

【中文标题】集成测试将整个对象发布到 Spring MVC 控制器【英文标题】:Integration Testing POSTing an entire object to Spring MVC controller 【发布时间】:2013-06-13 03:31:39 【问题描述】:

在集成测试 spring mvc web 应用程序时,有没有办法在模拟请求上传递整个表单对象?我能找到的只是将每个字段分别作为这样的参数传递:

mockMvc.perform(post("/somehwere/new").param("items[0].value","value"));

这适用于小型表单。但是,如果我发布的对象变大了怎么办?如果我可以发布整个对象,它也会使测试代码看起来更好。

具体来说,我想通过复选框测试多个项目的选择,然后发布它们。当然我可以只测试发布一个项目,但我想知道..

我们正在使用包含 spring-test-mvc 的 spring 3.2.2。

我的表单模型如下所示:

NewObject 
    List<Item> selection;

我尝试过这样的调用:

mockMvc.perform(post("/somehwere/new").requestAttr("newObject", newObject) 

到这样的控制器:

@Controller
@RequestMapping(value = "/somewhere/new")
public class SomewhereController 

    @RequestMapping(method = RequestMethod.POST)
    public String post(
            @ModelAttribute("newObject") NewObject newObject) 
        // ...
    

但是对象会是空的(是的,我之前在测试中填充过)

我找到的唯一可行的解​​决方案是像这样使用@SessionAttribute: Integration Testing of Spring MVC Applications: Forms

但我不喜欢必须记住在我需要它的每个控制器的末尾调用完成的想法。毕竟表单数据不必在会话中,我只需要一个请求。

所以我现在唯一能想到的就是编写一些 Util 类,它使用 MockHttpServletRequestBuilder 将所有对象字段附加为 .param 使用反射或单独为每个测试用例..

我不知道,感觉不直观..

关于如何让我的点赞更轻松有什么想法/想法? (除了直接调用控制器)

谢谢!

【问题讨论】:

尝试使用 gson 并将对象转换为 json 并发布 ?? 这有什么帮助?我的表单将发布MediaType.APPLICATION_FORM_URLENCODED 数据,因此我的测试应该发送该数据.. 我什至尝试从我发布的链接进行转换,将字节 [] 发送到控制器,但它仍然无法接收它.. 【参考方案1】:

我有同样的问题,结果发现解决方案相当简单,使用 JSON 编组器。 让您的控制器通过将@ModelAttribute("newObject") 更改为@RequestBody 来更改签名。像这样:

@Controller
@RequestMapping(value = "/somewhere/new")
public class SomewhereController 

    @RequestMapping(method = RequestMethod.POST)
    public String post(@RequestBody NewObject newObject) 
        // ...
    

那么在你的测试中你可以简单地说:

NewObject newObjectInstance = new NewObject();
// setting fields for the NewObject  

mockMvc.perform(MockMvcRequestBuilders.post(uri)
  .content(asJsonString(newObjectInstance))
  .contentType(MediaType.APPLICATION_JSON)
  .accept(MediaType.APPLICATION_JSON));

asJsonString 方法只是:

public static String asJsonString(final Object obj) 
    try 
        final ObjectMapper mapper = new ObjectMapper();
        final String jsonContent = mapper.writeValueAsString(obj);
        return jsonContent;
     catch (Exception e) 
        throw new RuntimeException(e);
    
  

【讨论】:

太糟糕了,也许 Spring 应该像 RestAssured 一样支持 .content(Object o) 调用 REST-assured 看起来很不错,但我还没有尝试过。感谢您提及。 不完整的例子,我从哪里得到“post”方法? @Siddharth 感谢您的反馈!它来自import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;。我更新了我的答案。 使用 RequestBody 改变 ModelAttribute 和 JSON 用于 body 假设所有客户端都发生了变化。这些客户端现在使用“application/x-www-form-urlencoded”内容类型,而不是 JSON。【参考方案2】:

使用MockMvc 进行集成测试的主要目的之一是验证模型对象是否正确填充了表单数据。

为了做到这一点,您必须传递从实际表单传递的表单数据(使用.param())。如果您使用某种从NewObject 到数据的自动转换,您的测试将不会涵盖特定类别的可能问题(NewObject 的修改与实际形式不兼容)。

【讨论】:

是的,也有类似的想法。另一方面,我实际上并没有测试表单本身,我只是假设我在测试中通过的参数是实际存在于表单中,所以当我更改模型和测试时,表单可能仍然存在不兼容问题,所以我想,为什么还要测试它..?! 我发现这里的解决方案很有帮助:***.com/questions/36568518/…【参考方案3】:

我相信我使用 Spring Boot 1.4 得到了最简单的答案,包括测试类的导入。:

public class SomeClass   /// this goes in it's own file
//// fields go here


import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.http.MediaType
import org.springframework.test.context.junit4.SpringRunner
import org.springframework.test.web.servlet.MockMvc

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status

@RunWith(SpringRunner.class)
@WebMvcTest(SomeController.class)
public class ControllerTest 

  @Autowired private MockMvc mvc;
  @Autowired private ObjectMapper mapper;

  private SomeClass someClass;  //this could be Autowired
                                //, initialized in the test method
                                //, or created in setup block
  @Before
  public void setup() 
    someClass = new SomeClass(); 
  

  @Test
  public void postTest() 
    String json = mapper.writeValueAsString(someClass);
    mvc.perform(post("/someControllerUrl")
       .contentType(MediaType.APPLICATION_JSON)
       .content(json)
       .accept(MediaType.APPLICATION_JSON))
       .andExpect(status().isOk());
  


【讨论】:

【参考方案4】:

我认为这些解决方案中的大多数都过于复杂。 我假设在你的测试控制器中你有这个

 @Autowired
 private ObjectMapper objectMapper;

如果是休息服务

@Test
public void test() throws Exception 
   mockMvc.perform(post("/person"))
          .contentType(MediaType.APPLICATION_JSON)
          .content(objectMapper.writeValueAsString(new Person()))
          ...etc

对于使用张贴表单的 spring mvc,我想出了这个解决方案。 (还不确定这是否是个好主意)

private MultiValueMap<String, String> toFormParams(Object o, Set<String> excludeFields) throws Exception 
    ObjectReader reader = objectMapper.readerFor(Map.class);
    Map<String, String> map = reader.readValue(objectMapper.writeValueAsString(o));

    MultiValueMap<String, String> multiValueMap = new LinkedMultiValueMap<>();
    map.entrySet().stream()
            .filter(e -> !excludeFields.contains(e.getKey()))
            .forEach(e -> multiValueMap.add(e.getKey(), (e.getValue() == null ? "" : e.getValue())));
    return multiValueMap;




@Test
public void test() throws Exception 
  MultiValueMap<String, String> formParams = toFormParams(new Phone(), 
  Set.of("id", "created"));

   mockMvc.perform(post("/person"))
          .contentType(MediaType.APPLICATION_FORM_URLENCODED)
          .params(formParams))
          ...etc

基本思想是 - 首先将对象转换为 json 字符串以轻松获取所有字段名称 - 将此 json 字符串转换为地图并将其转储到 spring 期望的 MultiValueMap 中。可选择过滤掉您不想包含的任何字段(或者您可以使用 @JsonIgnore 注释字段以避免此额外步骤)

【讨论】:

这个问题显然与 REST 端点无关,因此您可以删除一半的答案。然后我们就剩下“不确定这是否是个好主意”部分 呃,你在说什么?当然它与 REST 有关。整个想法是在 spring mvc 的上下文中测试 REST 控制器。好吧,经过 2 年多的思考,我宣布这是一个好主意。 您在答案中写下“如果它是一个休息服务”,在您的最新评论中写下“当然它与 REST 相关”。你认为它们相互对应吗?我提供了足够的信息来理解我写的内容,而且问题的作者也这样做了。【参考方案5】:

另一种使用反射解决的方法,但无需编组:

我有这个抽象的辅助类:

public abstract class MvcIntegrationTestUtils 

       public static MockHttpServletRequestBuilder postForm(String url,
                 Object modelAttribute, String... propertyPaths) 

              try 
                     MockHttpServletRequestBuilder form = post(url).characterEncoding(
                           "UTF-8").contentType(MediaType.APPLICATION_FORM_URLENCODED);

                     for (String path : propertyPaths) 
                            form.param(path, BeanUtils.getProperty(modelAttribute, path));
                     

                     return form;

               catch (Exception e) 
                     throw new RuntimeException(e);
              
     

你可以这样使用它:

// static import (optional)
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

// in your test method, populate your model attribute object (yes, works with nested properties)
BlogSetup bgs = new BlogSetup();
      bgs.getBlog().setBlogTitle("Test Blog");
      bgs.getUser().setEmail("admin.localhost@example.com");
    bgs.getUser().setFirstName("Administrator");
      bgs.getUser().setLastName("Localhost");
      bgs.getUser().setPassword("password");

// finally put it together
mockMvc.perform(
            postForm("/blogs/create", bgs, "blog.blogTitle", "user.email",
                    "user.firstName", "user.lastName", "user.password"))
            .andExpect(status().isOk())

我推断在构建表单时最好能够提及属性路径,因为我需要在测试中改变它。例如,我可能想检查是否在缺少输入时收到验证错误,并且我将省略属性路径来模拟条件。我还发现在 @Before 方法中构建模型属性更容易。

BeanUtils 来自 commons-beanutils:

    <dependency>
        <groupId>commons-beanutils</groupId>
        <artifactId>commons-beanutils</artifactId>
        <version>1.8.3</version>
        <scope>test</scope>
    </dependency>

【讨论】:

【参考方案6】:

我不久前遇到了同样的问题,并在Jackson 的帮助下使用反射解决了这个问题。

首先使用对象上的所有字段填充地图。然后将这些映射条目作为参数添加到 MockHttpServletRequestBuilder。

通过这种方式,您可以使用任何对象并将其作为请求参数传递。我敢肯定还有其他解决方案,但这个对我们有用:

    @Test
    public void testFormEdit() throws Exception 
        getMockMvc()
                .perform(
                        addFormParameters(post(servletPath + tableRootUrl + "/" + POST_FORM_EDIT_URL).servletPath(servletPath)
                                .param("entityID", entityId), validEntity)).andDo(print()).andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON)).andExpect(content().string(equalTo(entityId)));
    

    private MockHttpServletRequestBuilder addFormParameters(MockHttpServletRequestBuilder builder, Object object)
            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException 

        SimpleDateFormat dateFormat = new SimpleDateFormat(applicationSettings.getApplicationDateFormat());

        Map<String, ?> propertyValues = getPropertyValues(object, dateFormat);

        for (Entry<String, ?> entry : propertyValues.entrySet()) 
            builder.param(entry.getKey(),
                    Util.prepareDisplayValue(entry.getValue(), applicationSettings.getApplicationDateFormat()));
        

        return builder;
    

    private Map<String, ?> getPropertyValues(Object object, DateFormat dateFormat) 
        ObjectMapper mapper = new ObjectMapper();
        mapper.setDateFormat(dateFormat);
        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        mapper.registerModule(new JodaModule());

        TypeReference<HashMap<String, ?>> typeRef = new TypeReference<HashMap<String, ?>>() ;

        Map<String, ?> returnValues = mapper.convertValue(object, typeRef);

        return returnValues;

    

【讨论】:

【参考方案7】:

这是我用来递归转换映射中对象字段的方法,准备与MockHttpServletRequestBuilder一起使用

public static void objectToPostParams(final String key, final Object value, final Map<String, String> map) throws IllegalAccessException 
    if ((value instanceof Number) || (value instanceof Enum) || (value instanceof String)) 
        map.put(key, value.toString());
     else if (value instanceof Date) 
        map.put(key, new SimpleDateFormat("yyyy-MM-dd HH:mm").format((Date) value));
     else if (value instanceof GenericDTO) 
        final Map<String, Object> fieldsMap = ReflectionUtils.getFieldsMap((GenericDTO) value);
        for (final Entry<String, Object> entry : fieldsMap.entrySet()) 
            final StringBuilder sb = new StringBuilder();
            if (!GenericValidator.isEmpty(key)) 
                sb.append(key).append('.');
            
            sb.append(entry.getKey());
            objectToPostParams(sb.toString(), entry.getValue(), map);
        
     else if (value instanceof List) 
        for (int i = 0; i < ((List) value).size(); i++) 
            objectToPostParams(key + '[' + i + ']', ((List) value).get(i), map);
        
    

GenericDTO 是一个扩展Serializable 的简单类

public interface GenericDTO extends Serializable 

这里是ReflectionUtils

public final class ReflectionUtils 
    public static List<Field> getAllFields(final List<Field> fields, final Class<?> type) 
        if (type.getSuperclass() != null) 
            getAllFields(fields, type.getSuperclass());
        
        // if a field is overwritten in the child class, the one in the parent is removed
        fields.addAll(Arrays.asList(type.getDeclaredFields()).stream().map(field -> 
            final Iterator<Field> iterator = fields.iterator();
            while(iterator.hasNext())
                final Field fieldTmp = iterator.next();
                if (fieldTmp.getName().equals(field.getName())) 
                    iterator.remove();
                    break;
                
            
            return field;
        ).collect(Collectors.toList()));
        return fields;
    

    public static Map<String, Object> getFieldsMap(final GenericDTO genericDTO) throws IllegalAccessException 
        final Map<String, Object> map = new HashMap<>();
        final List<Field> fields = new ArrayList<>();
        getAllFields(fields, genericDTO.getClass());
        for (final Field field : fields) 
            final boolean isFieldAccessible = field.isAccessible();
            field.setAccessible(true);
            map.put(field.getName(), field.get(genericDTO));
            field.setAccessible(isFieldAccessible);
        
        return map;
    

你可以像这样使用它

final MockHttpServletRequestBuilder post = post("/");
final Map<String, String> map = new TreeMap<>();
objectToPostParams("", genericDTO, map);
for (final Entry<String, String> entry : map.entrySet()) 
    post.param(entry.getKey(), entry.getValue());

我没有对它进行广泛的测试,但它似乎有效。

【讨论】:

以上是关于集成测试将整个对象发布到 Spring MVC 控制器的主要内容,如果未能解决你的问题,请参考以下文章

Spring MVC集成测试弹簧安全问题,怎么解决

将 Spring MVC Spring Security 与 Undertow Web 容器集成

spring mvc中的单元测试

Spring Transactions 导致 spring-mvc 集成测试失败

Spring MVC 与 Spring Security 的集成测试

使用 Spring-Test-MVC 单元测试 Spring-Security - 集成 FilterChain / ServletFilter