Test d'intégration affichage d'un objet entier dans le contrôleur MVC Spring
y a-t-il un moyen de passer un objet de formulaire entier à la demande simulée lorsque l'intégration teste une application web MVC de printemps? Tout ce que je peux trouver est de passer chaque domaine séparément comme un paramètre comme ceci:
mockMvc.perform(post("/somehwere/new").param("items[0].value","value"));
ce qui est très bien pour les petits formulaires. Mais que faire si mon objet posté devient plus grand? Aussi, il rend le code de test plus agréable si je peux juste poster un objet entier.
en particulier, je voudrais tester la sélection de plusieurs éléments par case à cocher et ensuite les poster. Bien sûr, je pourrais juste un test postant un seul article, mais je me demandais..
nous utilisons le ressort 3.2.2 avec le ressort-test-mvc inclus.
Mon Modèle pour la forme ressemble à quelque chose comme ceci:
NewObject {
List<Item> selection;
}
j'ai essayé appelle comme ceci:
mockMvc.perform(post("/somehwere/new").requestAttr("newObject", newObject)
pour un Contrôleur comme ceci:
@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, j'ai rempli avant dans le test)
la seule solution que j'ai trouvée était d'utiliser @SessionAttribute comme ceci: test D'intégration des applications MVC de printemps: formulaires
Mais je n'aime pas l'idée d'avoir à penser à appeler à la fin de chaque contrôleur où j'ai besoin de cela. Après toutes les données du formulaire ne doit pas être à l'intérieur de la session, j'ai seulement besoin pour la demande.
donc la seule chose à laquelle je peux penser maintenant est d'écrire une classe Util qui utilise le Muchtpservletrequestbuilder pour ajouter tous les champs objet as .param en utilisant des réflexions ou individuellement pour chaque cas de test..
Je ne sais pas, feeld pas intuitif..
y a-t-il des idées ou des idées sur la façon dont je pourrais rendre ma vie plus facile? (Mis à part le fait d'appeler directement le contrôleur)
Merci!
7 réponses
l'Un des principaux objectifs des tests d'intégration avec MockMvc
est de vérifier que les objets model sont correctement remplis avec les données du formulaire.
pour le faire, vous devez passer les données du formulaire car elles sont passées du formulaire actuel (en utilisant .param()
). Si vous utilisez une conversion automatique de NewObject
à partir de données, votre test ne couvrira pas la classe particulière de problèmes possibles (modifications de NewObject
incompatible avec la forme réelle).
j'ai eu la même question et il s'est avéré que la solution était assez simple, en utilisant JSON marshaller.
Le fait d'avoir votre controller change simplement la signature en changeant @ModelAttribute("newObject")
@RequestBody
. Comme ceci:
@Controller
@RequestMapping(value = "/somewhere/new")
public class SomewhereController {
@RequestMapping(method = RequestMethod.POST)
public String post(@RequestBody NewObject newObject) {
// ...
}
}
Puis 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));
asJsonString
méthode est juste:
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);
}
}
je crois que j'ai la réponse la plus simple encore en utilisant la botte de printemps 1.4, importations incluses 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());
}
}
une autre façon de résoudre avec réflexion, mais sans se regrouper:
j'ai ce abstraite de la classe helper:
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'utiliser 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("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())
j'ai déduit qu'il est préférable de pouvoir mentionner les chemins de propriété lors de la construction de la forme, puisque j'ai besoin de varier cela dans mes tests. Par exemple, je pourrais vouloir vérifier si je reçois une erreur de validation sur une Entrée manquante et je vais laisser le chemin de propriété pour simuler la condition. Je trouve aussi que c' plus facile de construire mes attributs de modèle dans une méthode @Before.
Le BeanUtils est de commons-beanutils:
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.8.3</version>
<scope>test</scope>
</dependency>
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 le printemps mvc à l'aide d'une forme validée j'ai trouvé cette solution. (Pas vraiment sûr si c'est une bonne idée)
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
- première conversion objet en chaîne json pour obtenir tous les noms de champ facilement
- convertissez cette chaîne json en une carte et la déverser dans un MultiValueMap
que le printemps s'attend. Éventuellement filtrer les champs que vous ne voulez pas inclure (Ou vous pouvez annoter les champs @JsonIgnore
pour éviter cette étape supplémentaire)
j'ai rencontré le même problème il y a quelque temps et je l'ai résolu en utilisant la réflexion avec un peu d'aide de Jackson.
tout d'abord peupler une carte avec tous les champs sur un objet. Ensuite, ajoutez ces entrées de carte comme paramètres à la MockHttpServletRequestBuilder.
de cette façon, vous pouvez utiliser n'importe quel objet et vous le Passez comme paramètres de requête. Je suis sûr qu'il y a d'autres solutions mais celle-ci a travaillé 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;
}
Voici la méthode que j'ai faite 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 qui s'étend Serializable
public interface GenericDTO extends Serializable {}
et voici le ReflectionUtils
classe
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 n'ai pas testé beaucoup, mais il semble fonctionner.