Manuel Jordan opened SPR-14722 and commented

I am working with:

  • Spring Framework 4.3.2

About Spring MVC Test for Json I have the following:

.andExpect(jsonPath("$.id").exists())
.andExpect(jsonPath("$.id").value(is("100")))
.andExpect(jsonPath("$.id").value(is(persona.getId())))
.andExpect(jsonPath("$.nombre").exists())
.andExpect(jsonPath("$.nombre").value(is("Jesús Você")))
.andExpect(jsonPath("$.nombre").value(is(persona.getNombre())))
.andExpect(jsonPath("$.apellido").exists())
.andExpect(jsonPath("$.apellido").value(is("Mão Nuñez")))
.andExpect(jsonPath("$.apellido").value(is(persona.getApellido())))
....

now consider a more complex object, either more fields or relation with other objects. With the following I have a considerable reduction of code

.andExpect(jsonPath("$", is(persona)))
.andExpect(jsonPath("$").value(is(persona)))

Same thought for collections of data returned in JSON format. Here jsonPath works in peace with Hamcrest

The problem is with XML. I have the following:

.andExpect(xpath("persona/*").nodeCount(is(4)))
.andExpect(xpath("persona/id").exists())
.andExpect(xpath("persona/id").string(is("100")))
.andExpect(xpath("persona/id").string(is(persona.getId())))
.andExpect(xpath("persona/nombre").exists())
.andExpect(xpath("persona/nombre").string(is("Jesús Você")))
.andExpect(xpath("persona/nombre").string(is(persona.getNombre())))
.andExpect(xpath("persona/apellido").exists())
.andExpect(xpath("persona/apellido").string(is("Mão Nuñez")))
.andExpect(xpath("persona/apellido").string(is(persona.getApellido())))

The code works or pass, but again consider a more complex object, either more fields or relation with other objects. Sadly xpath does not work with Hamcrest. So I am not able to do a reduction of code. Same problem for collections of data returned in XML format.

This problem has been discussed in some way in: SPR-13687 - Why MockMvcResultMatchers has not a xpath method with org.hamcrest.Matcher? Pls. read the latest comment

Considering that Spring Framework 5 is coming, perhaps would have sense in re-consider in apply this improvement?

Thanks by your understanding.


Affects: 4.3 GA, 4.3.1, 4.3.2, 5.0 M1

Issue Links: - #18262 Why MockMvcResultMatchers has not a xpath method with org.hamcrest.Matcher?

Backported to: Contributions Welcome

Comment From: spring-projects-issues

Rossen Stoyanchev commented

XPath returns basic data types as well as org.w3c.dom.Node / org.w3c.dom.NodeList. We could experiment turn that into a DOMSource and use Jaxb2Marshaller to unmarshal it.

Comment From: spring-projects-issues

Manuel Jordan commented

Sounds great

About:

We could experiment turn that into a DOMSource and use Jaxb2Marshaller to unmarshal it

I had the problem with JAXB2 that it does not support generic collections.

I must use Jackson for XML instead, the 'drawback' is that once used Jackson through a mandatory special module required for xml, the Jaxb2 annotations are ignored.

I am sharing this because because Spring 5 should work with Java 9, and I don't if the API for Jaxb2 would be changed or Jaxb 3 would be created...

Comment From: spring-projects-issues

Rossen Stoyanchev commented

Regarding JAXB2 and generic collections have you seen the Jaxb2CollectionHttpMessageConverter? Something along those lines perhaps.

Comment From: spring-projects-issues

Manuel Jordan commented

The special module is jackson-dataformat-xml, it from FasterXML/jackson-dataformat-xml

Now the problem is the following bug:

XML Empty tag to Empty string in the object during xml deserialization

Practically reflected in:

Spring Rest & Jackson: Empty values are transformed in Null values in XML transformation

How you can see Jaxb2 and Jackson have each one its own 'situations'

Comment From: spring-projects-issues

Manuel Jordan commented

About Jaxb2CollectionHttpMessageConverter I think I had tested and did not work... the reason? I can't remember

Not sure If I have reported through JIRA or Stackoverflow (I think the former) this situation and from either of them Jaxb2CollectionHttpMessageConverter was suggested, but did not work... the solution was Jackson with that special module.

Comment From: spring-projects-issues

Rossen Stoyanchev commented

I don't mean using the converter directly but doing something like what it does and iterate over a NodeList.

Comment From: rstoyanchev

Revisiting this issue, there are some things I overlooked. In order to compare to a higher level object, you need to specify a target class, so it should be this at least:

.andExpect(jsonPath("$", is(persona), Persona.class))

Even then, unless JSONPath is configured with a JsonProvider, the JsonSmartMappingProvider used by default only decodes to maps of objects. This was raised in #31423 and #27486 recently. We do have an improvement planned with #31651 for JsonPathExpectationsHelper to expose JSONPath for configuration. However, the general challenge is that with static methods for ResultMatcher implementations, there is no way to have JSON or XML library configured during setup, and the config would have to be passed in every time or configured statically, neither of which is a good solution.

This is why I don't think we can do much more here.

That said, I do actually have an alternative to suggest. If the goal is to compare higher level objects, you can use WebTestClient. Unlike MockMvc that does server side testing, WebTestClient is an actual test client that does encoding and decoding to higher level objects, and it can be used with any server, including MockMvc through MockMvcWebTestClient. You can use it to do something like:

@Test
void xml() {
    MockMvcWebTestClient.bindToController(new PersonController()).build()
            .get()
            .uri("/person/Lee")
            .accept(MediaType.APPLICATION_XML)
            .exchange()
            .expectStatus().isOk()
            .expectHeader().contentType(MediaType.APPLICATION_XML)
            .expectBody(Person.class)
            .isEqualTo(person);
}