IT story

통합 테스트 전체 객체를 Spring MVC 컨트롤러에 게시

hot-time 2021. 1. 6. 20:25
반응형

통합 테스트 전체 객체를 Spring MVC 컨트롤러에 게시


스프링 mvc 웹 앱을 통합 테스트 할 때 모의 요청에 전체 양식 객체를 전달하는 방법이 있습니까? 내가 찾을 수있는 것은 다음과 같은 매개 변수로 각 필드를 개별적으로 전달하는 것입니다.

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

작은 형태에는 괜찮습니다. 하지만 게시 된 개체가 커지면 어떻게됩니까? 또한 전체 객체를 게시 할 수 있다면 테스트 코드가 더 멋지게 보입니다.

특히 체크 박스를 통해 여러 항목의 선택을 테스트 한 다음 게시하고 싶습니다. 당연히 하나의 아이템 만 게시해볼 수 있지만 ..

spring-test-mvc가 포함 된 스프링 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를 다음과 같이 사용하는 것 입니다. Spring MVC 애플리케이션의 통합 테스트 : Forms

그러나 나는 이것이 필요한 모든 컨트롤러의 끝에서 complete 호출을 기억해야한다는 생각을 싫어합니다. 모든 양식 데이터가 세션 내부에있을 필요는 없지만 하나의 요청에만 필요합니다.

그래서 지금 당장 생각할 수있는 유일한 것은 MockHttpServletRequestBuilder를 사용하는 Util 클래스를 작성하여 리플렉션을 사용하여 모든 개체 필드를 .param으로 추가하거나 각 테스트 케이스에 대해 개별적으로 추가하는 것입니다.

몰라, 직관적이지 않은 느낌 ..

내가 좋아하는 것을 더 쉽게 만들 수있는 방법에 대한 생각 / 아이디어가 있습니까? (컨트롤러를 직접 호출하는 것 외에는)

감사!


통합 테스트의 주요 목적 중 하나는 MockMvc모델 개체가 양식 데이터로 올바르게 채워져 있는지 확인하는 것입니다.

이를 위해서는 실제 양식에서 전달되는 양식 데이터를 전달해야합니다 (사용 .param()). NewObject데이터에서 데이터로의 자동 변환을 사용하는 경우 테스트는 특정 클래스의 가능한 문제 ( NewObject실제 형식과 호환되지 않는 수정)를 다루지 않습니다 .


나는 똑같은 질문을했고 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 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());
  }

}

이러한 솔루션의 대부분은 너무 복잡하다고 생각합니다. 나는 당신의 테스트 컨트롤러에 이것을 가지고 있다고 가정합니다.

 @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이 추가 단계를 피하기 위해 필드에 주석을 달 수 있음 ).


리플렉션으로 해결하는 또 다른 방법이지만 마샬링은 없습니다.

이 추상 도우미 클래스가 있습니다.

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>

나는 얼마 전에 같은 문제를 만났고 Jackson의 도움을 받아 반사를 사용하여 해결했습니다 .

먼저 오브젝트의 모든 필드로 맵을 채 웁니다. 그런 다음 해당 맵 항목을 MockHttpServletRequestBuilder에 매개 변수로 추가합니다.

이러한 방식으로 모든 Object를 사용할 수 있으며 요청 매개 변수로 전달합니다. 나는 거기에 다른 해결책이 있다고 확신하지만 이것은 우리를 위해 일했습니다.

    @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;

    }

Here is the method I made to transform recursively the fields of an object in a map ready to be used with a 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 is a simple class extending Serializable

public interface GenericDTO extends Serializable {}

and here is the ReflectionUtils class

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

You can use it like

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

I didn't tested it extensively, but it seems to work.

ReferenceURL : https://stackoverflow.com/questions/17143116/integration-testing-posting-an-entire-object-to-spring-mvc-controller

반응형