2013-06-17 13 views
36

Czy istnieje sposób na przesłanie całego obiektu formularza na żądanie próbne podczas testowania integracji aplikacji internetowej Spring MVC? Wszystko, co mogę znaleźć, to przekazać każde pole osobno jako parametr podobny do tego:Testowanie integracyjne WPROWADZENIE całego obiektu do kontrolera Spring MVC

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

Co jest dobre dla małych formularzy. Ale co, jeśli mój opublikowany obiekt stanie się większy? Również sprawia, że ​​kod testowy wygląda ładniej, jeśli mogę po prostu umieścić cały obiekt.

W szczególności chciałbym przetestować wybór wielu przedmiotów za pomocą pola wyboru, a następnie opublikować je. Oczywiście mogłem po prostu przetestować wysłanie pojedynczego przedmiotu, ale zastanawiałem się.

Używamy sprężyny 3.2.2 z dołączonym testem sprężynowym-mvc.

Moja Wzór formularza wyglądać tak:

NewObject { 
    List<Item> selection; 
} 

próbowałam połączenia tak:

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

do kontrolera jak ten:

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

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

Ale obiekt będzie pusty (tak, wypełniłem go wcześniej w teście)

Rozwiązanie działa tylko znalazłem używał @SessionAttribute tak: Integration Testing of Spring MVC Applications: Forms

Ale ja lubię ideę konieczności pamiętania zadzwonić zakończona pod koniec każdego kontrolera, gdzie potrzebują tego. Po tym, jak wszystkie dane formularza nie muszą znajdować się w sesji, potrzebuję go tylko na jedno żądanie.

Więc jedyne co mogę myśleć teraz jest napisać jakąś klasę Util który używa MockHttpServletRequestBuilder dołączyć wszystkie pola obiektów jak .param użyciu odbicia lub indywidualnie dla każdego przypadku testowego ..

I don” Wiem, czuć się nieintuicyjnie ..

Jakieś przemyślenia/pomysły na to, w jaki sposób mogę ułatwić moje życie? (Poza bezpośrednim wywołaniem kontrolera)

Dzięki!

+0

spróbuj użyć gson i przekonwertować obiekt na json i opublikować go? – DarthCoder

+0

jak to pomoże? Mój formularz opublikuje dane "MediaType.APPLICATION_FORM_URLENCODED", więc mój test powinien wysłać te dane. Próbowałem nawet konwertować z linku, który wysyłam pocztą bajtową [] do kontrolera, ale nadal go nie odbierze .. – Pete

Odpowiedz

14

Jednym z głównych celów testowania integracji z MockMvc jest sprawdzenie, czy obiekty modelu są poprawnie wypełnione danymi formularza.

W tym celu należy przekazać dane formularza, ponieważ są one przekazywane z rzeczywistego formularza (przy użyciu .param()). Jeśli użyjesz automatycznej konwersji z NewObject z danych, test nie będzie obejmował konkretnej klasy możliwych problemów (modyfikacje NewObject są niezgodne z rzeczywistą formą).

+1

Tak, myślałam również wzdłuż tych linii. Z drugiej strony, tak naprawdę nie testuję samej formy, zakładam tylko, że parametry, które przechodzę w teście, są w rzeczywistości obecne w formie, więc kiedy Zmieniam swój model i test, forma może nadal mieć problemy z niekompatybilnością, więc pomyślałem, dlaczego nawet przetestować ...?! – Pete

+0

Znalazłem rozwiązanie tutaj pomocne: http://stackoverflow.com/questions/36568518/testing-form-posts-through-mockmvc –

2

Wpadłem na ten sam problem jakiś czas temu i rozwiązałem go za pomocą refleksji z pomocą Jackson.

Najpierw wypełnij mapę wszystkimi polami obiektu. Następnie dodaj te wpisy map jako parametry do MockHttpServletRequestBuilder.

W ten sposób można użyć dowolnego obiektu i przekazujesz go jako parametry żądania.Jestem pewien, że istnieją inne rozwiązania tam, ale ten pracował dla nas:

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

    } 
5

Innym sposobem rozwiązania z refleksji, ale bez zestawiania:

mam tej abstrakcyjnej klasy pomocnika:

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

go używać tak:

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

mam wydedukować, że lepiej jest być w stanie t o wspominać o ścieżkach właściwości podczas budowania formularza, ponieważ muszę to zmienić w moich testach. Na przykład, mógłbym chcieć sprawdzić, czy dostaję błąd sprawdzania poprawności na brakujących danych wejściowych, a ja pominę ścieżkę właściwości, aby zasymulować warunek. Również łatwiej jest budować moje atrybuty modelu w metodzie @Before.

W BeanUtils pochodzi z Commons beanutils:

<dependency> 
     <groupId>commons-beanutils</groupId> 
     <artifactId>commons-beanutils</artifactId> 
     <version>1.8.3</version> 
     <scope>test</scope> 
    </dependency> 
+0

getDate nie działa? –

37

Miałem to samo pytanie i okazało się rozwiązanie było dość proste, za pomocą JSON naziemnego.
Posiadanie sterownika wystarczy zmienić podpis, zmieniając @ModelAttribute("newObject") na @RequestBody. Tak:

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

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

Następnie w badaniach można po prostu powiedzieć:

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

Jeżeli metoda asJsonString tylko:

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

szkoda, może wiosna powinna obsługiwać wywołanie .content (Object o) jak RestAssured robi – tbraun

+1

Zapewniony przez REST wygląda całkiem ładnie, ale jeszcze go nie wypróbowałem. Dzięki za wzmiankę o tym. – nyxz

+0

ten sam problem, ale ten przykład nie działał dla mnie – emoleumassi

1

Oto metoda zrobiłem przekształcić rekurencyjnie pola obiektu na mapie gotowe do użycia z urządzeniem 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 to prosta klasa rozszerzenie Serializable

public interface GenericDTO extends Serializable {} 

i tutaj jest ReflectionUtils klasa

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

Można go używać jak

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

nie testowałem go obszernie , ale wygląda na to, że działa.

10

wierzę, że mam jeszcze najprostszą odpowiedź użyciu Wiosna Boot 1.4, zawarte importu dla klasy testowej .:

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

} 
1

Myślę, że większość z tych rozwiązań nie są zbyt skomplikowane. Zakładam, że w kontrolerze testu masz ten

@Autowired 
private ObjectMapper objectMapper; 

jeśli jego usługi odpoczynek

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

na wiosnę MVC za pomocą zamieszczonych formularz wymyśliłem tego rozwiązania. (Nie do końca pewien, czy jest to dobre jeszcze pomysł)

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 
} 

Podstawową ideą jest - najpierw przekonwertować obiekt do json ciąg łatwo uzyskać wszystkie nazwy pól - przekonwertować ten ciąg json na mapie i zrzucić go do MultiValueMap, którego oczekuje wiosna. Opcjonalnie odfiltruj wszystkie pola, których nie chcesz uwzględnić (lub możesz po prostu opisać pola za pomocą @JsonIgnore, aby uniknąć tego dodatkowego kroku)

Powiązane problemy