Подтвердить что ты не робот

Интеграция Тестирование POSTing всего объекта на Spring MVC-контроллер

Есть ли способ передать весь объект формы на mock-запрос при интеграции тестирования веб-приложения spring mvc? Все, что я могу найти, состоит в том, чтобы передать каждое поле отдельно как param:

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

Это хорошо для небольших форм. Но что, если мой размещенный объект станет больше? Кроме того, он делает тестовый код более приятным, если я могу просто разместить весь объект.

В частности, я хотел бы проверить выбор нескольких элементов, установив флажок, а затем разместив их. Конечно, я мог бы просто протестировать публикацию одного элемента, но мне было интересно..

Мы используем spring 3.2.2 с включенным spring -test-mvc.

Моя модель для формы выглядит примерно так:

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 следующим образом: Тестирование интеграции spring Приложения MVC: формы

Но мне не нравится идея помнить, что нужно позвонить в конце каждого контроллера, где мне это нужно. После того, как все данные формы не должны быть внутри сеанса, мне нужно только это для одного запроса.

Итак, единственное, что я могу сейчас подумать, это написать некоторый класс Util, который использует MockHttpServletRequestBuilder для добавления всех полей объекта в виде .param с использованием отражений или индивидуально для каждого тестового примера.

Я не знаю, feeld un-intuitive..

Любые мысли/идеи о том, как я мог бы сделать мою работу проще? (Помимо прямого вызова контроллера напрямую)

Спасибо!

4b9b3361

Ответ 1

Одной из основных целей тестирования интеграции с MockMvc является проверка того, что объекты модели заполнены данными формы.

Для этого вам необходимо передать данные формы, поскольку они передаются из фактической формы (используя .param()). Если вы используете некоторое автоматическое преобразование из NewObject в данные, ваш тест не будет охватывать определенный класс возможных проблем (модификации NewObject несовместимы с фактической формой).

Ответ 2

У меня был тот же вопрос, и оказалось, что решение было довольно простым, используя JSON marshaller.
Если ваш контроллер просто изменил подпись, изменив @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);
    }
}  

Ответ 3

Я считаю, что у меня есть самый простой ответ, но с использованием Spring Boot 1.4, включая импорт для тестового класса.:

public class SomeClass {  /// this goes in it 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

Другой способ решить с помощью Reflection, но без сортировки:

У меня есть этот абстрактный класс-помощник:

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("[email protected]");
    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.

The BeanUtils - это общедоступные вещи:

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

Ответ 5

Я столкнулся с тем же вопросом некоторое время назад и решил его с помощью рефлексии с помощью 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;

    }

Ответ 6

Вот метод, который я сделал для преобразования рекурсивно полей объекта в карте, готовой для использования с 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 {}

и вот < <25 > класс

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());
}

Я не тестировал его широко, но, похоже, он работает.

Ответ 7

Я думаю, что большинство из этих решений слишком сложны. Я предполагаю, что в вашем тестовом контроллере у вас есть этот

 @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 в карту и выгрузить ее в MultiValueMap, который ожидает spring. Необязательно отфильтровывайте любые поля, которые вы не хотите включать (или вы можете просто аннотировать поля с помощью @JsonIgnore, чтобы избежать этого дополнительного шага)