it-swarm.com.de

Integrationstest POST eines gesamten Objekts an Spring MVC Controller

Gibt es eine Möglichkeit, ein gesamtes Formularobjekt auf Scheinanforderung zu übergeben, wenn die Integration einer Spring MVC-Webanwendung getestet wird? Alles, was ich finden kann, ist, jedes Feld einzeln als Parameter wie folgt zu übergeben:

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

Welches ist gut für kleine Formen. Was aber, wenn mein eingestelltes Objekt größer wird? Außerdem sieht der Testcode besser aus, wenn ich nur ein gesamtes Objekt posten kann.

Insbesondere möchte ich die Auswahl mehrerer Elemente durch Aktivieren und anschließendes Posten testen. Natürlich konnte ich nur testen, einen einzelnen Artikel zu posten, aber ich habe mich gefragt ..

Wir verwenden Spring 3.2.2 mit dem mitgelieferten Spring-Test-MVC.

Mein Modell für das Formular sieht ungefähr so ​​aus:

NewObject {
    List<Item> selection;
}

Ich habe versucht, Anrufe wie folgt:

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

zu einem Controller wie diesem:

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

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

Aber das Objekt wird leer sein (ja, ich habe es vorher im Test gefüllt)

Die einzige funktionierende Lösung, die ich gefunden habe, war die Verwendung von @SessionAttribute wie folgt: Integrationstest von Spring MVC-Anwendungen: Formulare

Aber ich mag die Idee nicht, mich daran erinnern zu müssen, am Ende jedes Controllers, wo ich das brauche, komplett aufzurufen. Nachdem alle Formulardaten nicht in der Sitzung sein müssen, benötige ich sie nur für die eine Anfrage.

Das Einzige, woran ich jetzt denken kann, ist, eine Util-Klasse zu schreiben, die den MockHttpServletRequestBuilder verwendet, um alle Objektfelder als .param unter Verwendung von Reflexionen oder einzeln für jeden Testfall anzuhängen.

Ich weiß nicht, fühlte mich nicht intuitiv.

Irgendwelche Gedanken/Ideen, wie ich es mir leichter machen könnte? (Abgesehen davon, dass nur der Controller direkt angerufen wird)

Vielen Dank!

54
Pete

Einer der Hauptzwecke des Integrationstests mit MockMvc besteht darin, zu überprüfen, ob die Modellobjekte korrekt mit Formulardaten gefüllt sind.

Dazu müssen Sie Formulardaten so übergeben, wie sie aus dem tatsächlichen Formular stammen (mit .param()). Wenn Sie eine automatische Konvertierung von NewObject in von Daten verwenden, deckt Ihr Test keine bestimmte Klasse möglicher Probleme ab (Änderungen von NewObject sind mit der tatsächlichen Form nicht kompatibel).

23
axtavt

Ich hatte die gleiche Frage und es stellte sich heraus, dass die Lösung mit JSON Marshaller ziemlich einfach war.
Wenn Ihr Controller nur die Signatur ändert, ändern Sie @ModelAttribute("newObject") in @RequestBody. So was:

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

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

Dann können Sie in Ihren Tests einfach sagen:

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

Wo die asJsonString Methode gerade ist:

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

Ich glaube, ich habe die bisher einfachste Antwort mit Spring Boot 1.4, inklusive Importe für die Testklasse:

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

Ich denke, die meisten dieser Lösungen sind viel zu kompliziert. Ich gehe davon aus, dass Sie dies in Ihrem Test-Controller haben

 @Autowired
 private ObjectMapper objectMapper;

Wenn es ein Restdienst ist

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

Für spring mvc mit einem geposteten Formular Ich habe diese Lösung gefunden. (Bin mir nicht sicher, ob es noch eine gute Idee ist)

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
}

Die Grundidee besteht darin, zuerst ein Objekt in einen JSON-String umzuwandeln, um alle Feldnamen einfach zu erhalten. Dann diesen JSON-String in eine Map umzuwandeln und in einen MultiValueMap zu speichern, den Spring erwartet. Optional können Sie alle Felder herausfiltern, die Sie nicht einschließen möchten (oder Sie können Felder einfach mit @JsonIgnore Kommentieren, um diesen zusätzlichen Schritt zu vermeiden).

10
reversebind

Eine andere Möglichkeit, mit Reflection zu lösen, aber ohne Marshalling:

Ich habe diese abstrakte Helferklasse:

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

Du benutzt es so:

// 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())

Ich habe festgestellt, dass es besser ist, die Eigenschaftspfade beim Erstellen des Formulars zu erwähnen, da ich dies bei meinen Tests ändern muss. Ich möchte beispielsweise prüfen, ob bei einer fehlenden Eingabe ein Überprüfungsfehler auftritt, und den Eigenschaftspfad weglassen, um die Bedingung zu simulieren. Ich finde es auch einfacher, meine Modellattribute in einer @Before-Methode zu erstellen.

Die BeanUtils sind von commons-beanutils:

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

Ich bin vor einiger Zeit auf dasselbe Problem gestoßen und habe es mit Hilfe von Jackson durch Reflektion gelöst.

Füllen Sie zuerst eine Karte mit allen Feldern eines Objekts aus. Fügen Sie dann diese Zuordnungseinträge als Parameter zum MockHttpServletRequestBuilder hinzu.

Auf diese Weise können Sie jedes Objekt verwenden und es als Anforderungsparameter übergeben. Ich bin mir sicher, dass es andere Lösungen gibt, aber diese funktionierten für uns:

    @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

Hier ist die Methode, die ich gemacht habe, um die Felder eines Objekts in einer Map rekursiv zu transformieren, die bereit ist, mit einem MockHttpServletRequestBuilder verwendet zu werden.

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 ist eine einfache Klasse, die Serializable erweitert

public interface GenericDTO extends Serializable {}

und hier ist die Klasse 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;
    }
}

Du kannst es gerne benutzen

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

Ich habe es nicht ausgiebig getestet, aber es scheint zu funktionieren.

1
Dario Zamuner