web-dev-qa-db-fra.com

Intégration Test POST d'un objet entier sur le contrôleur Spring MVC

Existe-t-il un moyen de transmettre un objet de formulaire entier à une demande fictive lors de l’intégration du test d’une application Web mvc de ressort? Tout ce que je peux trouver est de passer chaque champ séparément en tant que paramètre comme ceci:

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

Ce qui est bien pour les petites formes. Mais que se passe-t-il si mon objet posté grossit? En outre, cela rend le code de test plus agréable si je peux simplement poster un objet entier.

Plus précisément, j'aimerais tester la sélection de plusieurs éléments en les cochant puis en les publiant. Bien sûr, je pouvais juste essayer de poster un seul article, mais je me demandais ..

Nous utilisons le printemps 3.2.2 avec le test-ressort-mvc inclus.

Mon modèle pour le formulaire ressemble à ceci:

NewObject {
    List<Item> selection;
}

J'ai essayé des appels comme celui-ci:

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

à un contrôleur comme celui-ci:

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

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

Mais l'objet sera vide (oui je l'ai déjà rempli dans le test)

La seule solution de travail que j'ai trouvée utilisait @SessionAttribute comme ceci: Test d'intégration d'applications Spring MVC: Forms

Mais je n'aime pas l'idée de devoir me rappeler d'appeler complètement à la fin de chaque contrôleur où j'ai besoin de cela. Après tout, les données de formulaire ne doivent pas nécessairement être à l'intérieur de la session, je n'en ai besoin que pour une requête.

Donc, la seule chose à laquelle je peux penser maintenant, c’est d’écrire une classe Util qui utilise MockHttpServletRequestBuilder pour ajouter tous les champs d’objet en tant que .param à l’aide de réflexions ou individuellement pour chaque cas de test.

Je ne sais pas, je ne me sentais pas intuitif ..

Des idées/idées sur la façon dont je pourrais rendre mon genre plus facile? (En plus d'appeler directement le contrôleur)

Merci!

54
Pete

L'un des principaux objectifs des tests d'intégration avec MockMvc est de vérifier que les objets de modèle sont correctement remplis avec les données de formulaire.

Pour ce faire, vous devez transmettre les données de formulaire comme elles ont été transmises à partir de la forme réelle (en utilisant .param()). Si vous utilisez une conversion automatique de NewObject en données, votre test ne couvrira pas une classe de problèmes possibles (modifications de NewObject incompatibles avec la forme réelle).

23
axtavt

J'avais la même question et il s'est avéré que la solution était assez simple, en utilisant JSON marshaller.
Si votre contrôleur modifie simplement la signature en remplaçant @ModelAttribute("newObject") par @RequestBody. Comme ça:

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

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

Ensuite, dans vos tests, vous pouvez simplement dire:

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

Où la méthode asJsonString est simplement:

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

Je pense avoir la réponse la plus simple à ce jour avec Spring Boot 1.4, importation incluse pour la classe de test .:

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

Je pense que la plupart de ces solutions sont beaucoup trop compliquées. Je suppose que dans votre contrôleur de test, vous avez ceci

 @Autowired
 private ObjectMapper objectMapper;

Si c'est un service de repos

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

Pour spring mvc en utilisant un formulaire posté Je suis venu avec cette solution. (Pas vraiment sûr si c'est une bonne idée pour le moment)

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
}

L'idée de base est de - convertir d'abord l'objet en chaîne json pour obtenir facilement tous les noms de champs - convertir cette chaîne json en une carte et la vider dans un MultiValueMap attendu par le printemps. Vous pouvez éventuellement filtrer les champs que vous ne souhaitez pas inclure (ou vous pouvez simplement annoter les champs avec @JsonIgnore pour éviter cette étape supplémentaire)

10
reversebind

Une autre façon de résoudre avec Reflection, mais sans trier:

J'ai cette classe d'assistance abstraite:

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

Vous l'utilisez comme ceci:

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

J'en ai déduit qu'il est préférable de pouvoir mentionner les chemins de propriétés lors de la création du formulaire, car je dois le modifier dans mes tests. Par exemple, je voudrais vérifier si une erreur de validation est générée sur une entrée manquante et laisser de côté le chemin de la propriété pour simuler la condition. Je trouve également plus facile de créer les attributs de mon modèle dans une méthode @Before.

BeanUtils provient de commons-beanutils:

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

J'ai rencontré le même problème il y a un moment et je l'ai résolu en utilisant la réflexion avec l'aide de Jackson .

Remplissez d'abord une carte avec tous les champs d'un objet. Ajoutez ensuite ces entrées de mappe en tant que paramètres au MockHttpServletRequestBuilder.

De cette façon, vous pouvez utiliser n'importe quel objet et vous le transmettez en tant que paramètre de requête. Je suis sûr qu'il existe d'autres solutions mais celle-ci a fonctionné pour nous:

    @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

Voici la méthode que j'ai créée pour transformer de manière récursive les champs d'un objet dans une carte prête à être utilisée avec un 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 est une classe simple s'étendant de Serializable

public interface GenericDTO extends Serializable {}

et voici la classe 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;
    }
}

Vous pouvez l'utiliser comme

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

Je ne l'ai pas testé de manière approfondie, mais cela semble fonctionner.

1
Dario Zamuner