web-dev-qa-db-ja.com

オブジェクト全体をSpring MVCコントローラーにPOSTする統合テスト

Spring MVC Webアプリを統合テストするときに、モックリクエストでフォームオブジェクト全体を渡す方法はありますか?私が見つけることができるのは、次のようなパラメータとして各フィールドを個別に渡すことです:

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

これは小さなフォームには適しています。しかし、投稿されたオブジェクトが大きくなったらどうなりますか?また、オブジェクト全体を投稿するだけでテストコードが見やすくなります。

具体的には、チェックボックスで複数のアイテムの選択をテストし、それらを投稿したいと思います。もちろん、1つのアイテムの投稿をテストすることもできましたが、疑問に思っていました。

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を使用することでした: Spring MVCアプリケーションの統合テスト:フォーム

しかし、私はこれを必要とするすべてのコントローラーの最後にcompleteを呼び出すことを覚えておく必要があるという考えを嫌います。すべてのフォームデータがセッション内にある必要はないので、1つの要求に対してのみ必要です。

したがって、私が今考えることができるのは、MockHttpServletRequestBuilderを使用するUtilクラスを作成して、すべてのオブジェクトフィールドをリフレクションを使用して、またはテストケースごとに個別に.paramとして追加することです。

わかりませんが、直感的ではないと感じました。

自分の好きなものをもっと簡単にする方法についての考え/アイデア(コントローラーを直接呼び出すだけでなく)

ありがとう!

54
Pete

MockMvcとの統合テストの主な目的の1つは、モデルオブジェクトにフォームデータが正しく入力されていることを確認することです。

それを行うには、実際のフォームから渡されるフォームデータを渡す必要があります(.param()を使用)。 NewObjectからデータへの自動変換を使用する場合、テストは特定のクラスの問題の可能性をカバーしません(実際のフォームと互換性のないNewObjectの変更)。

23
axtavt

私も同じ質問をしましたが、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);
    }
}  
56
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
}

ポストされたフォームを使用する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にダンプします。必要に応じて、含めたくないフィールドを除外します(または、フィールドに@JsonIgnoreこの余分なステップを避けるため)

10
reversebind

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はcommons-beanutilsからのものです。

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

私は少し前に同じ問題に遭遇し、 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;

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

GenericDTOSerializableを拡張する単純なクラスです

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