it-swarm.com.ru

Интеграционное тестирование Размещение всего объекта на контроллере Spring MVC

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

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 с использованием отражений или индивидуально для каждого тестового примера.

Я не знаю, чувствовал себя не интуитивно ..

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

Спасибо!

52
Pete

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

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

21
axtavt

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

Я полагаю, что у меня есть самый простой ответ, используя 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());
  }

}
19
Michael Harris

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

 @Autowired
 private ObjectMapper objectMapper;

Если это служба отдыха

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

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

8
ms.one

Еще один способ решить с помощью 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.

BeanUtils из общего достояния beanutils:

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

Я столкнулся с той же проблемой некоторое время назад и решил ее, используя рефлексию с некоторой помощью Джексон .

Сначала заполните карту всеми полями на объекте. Затем добавьте эти записи карты в качестве параметров в 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;

    }
3
phantastes

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

Я не проверял это всесторонне, но это, кажется, работает.

1
Dario Zamuner