DEV Community

loading...

Json conversion errors with Spring MockMvc

piczmar_0 profile image Marcin Piczkowski ・2 min read

MockMvc is a Spring class used for unit testing controllers without necessity to start server.
It has a fluent API for asserting response messages. E.g.: for JSON response we can write assertions like that:

 this.mockMvc.perform(get("/test"))
                .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.value", is("Hello!")));

Underneath it's using jsonPath.

It works nicely for String values assertions but can behave weird with numeric values.

Here is an example:

When testing controller method:


    public static final double VALUE = 0.07185454505642408;

...

    @GetMapping(value = "/test", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    public ResponseEntity get() {
        Dto dto = new Dto();
        dto.setValue(VALUE);
        return new ResponseEntity(dto, HttpStatus.OK);
    }

using test:

@RunWith(SpringRunner.class)
@WebMvcTest(ExampleController.class)
public class ExampleControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void should_get_metadata_when_video_processed() throws Exception {
        this.mockMvc.perform(get("/test"))
                .andDo(print())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.value", is(ExampleController.VALUE)));
    }

}

the test fails with:

java.lang.AssertionError: JSON path "$.value"
Expected: is <0.07185454505642408>
     but: was <0.07185454505642408>
Expected :is <0.07185454505642408>

Actual   :<0.07185454505642408>

Wow ! Both printed values, expected and actual, are the same.
What's going on?

In fact the problem is the type of expected and actual.
The expected is Double but the actual is BigDecimal.

I wouldn't have known it if I had not debugged it.

The reason is that MockMvc is using a JsonSmartJsonProvider for serialization.

When value is small, e.g. for Long it can be down-casted to Integer, for BigDecimal - to Double, then this is how JsonSmartJsonProvider works.

It results in unexpected situations when we serialize values of specific type to JSON but it cannot be deserialized back to the same numeric type.

What is a solution then?

There is an another JSON provider which can be used instead - a JacksonJsonProvider which is predictive because
you can configure it to always deserialize numeric values to Long and Double.

It's a pity that it is not the default implementation used in jsonPath.

Here is a little Github project with some tests demonstrating issues with JsonSmartJsonProvider and how to solve them withJacksonJsonProvider

In nutshell, the solution is simple.

You need to add this config in your tests initialization:


        final ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.enable(DeserializationFeature.USE_LONG_FOR_INTS);
        objectMapper.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS);

        Configuration.setDefaults(new Configuration.Defaults() {

            private final JsonProvider jsonProvider = new JacksonJsonProvider(objectMapper);
            private final MappingProvider mappingProvider = new JacksonMappingProvider(objectMapper);

            @Override
            public JsonProvider jsonProvider() {
                return jsonProvider;
            }

            @Override
            public MappingProvider mappingProvider() {
                return mappingProvider;
            }

            @Override
            public Set<Option> options() {
                return EnumSet.noneOf(Option.class);
            }
        });

By setting DeserializationFeature you get integer values deserialized always to Long and float always to BigDecimal.

Hope this helps you to save some time on solving the weird errors I faced.

Discussion (7)

pic
Editor guide
Collapse
rakibulbashar profile image
rakibul-bashar

Dear sir,
Your article is awesome.
I faced this problem when i writing a test. but i can not solved it yet.
if possible help me out.
Here is my question link
stackoverflow.com/questions/635395...
Thanks

Collapse
laptevn profile image
Nickolay Laptev

Are you sure we can "unit test" a controller?

Another point.
On a controller or endpoint layer we operate with requests and responses. Hence we have a response body in our testing scenario that is a plain text. Text is transferred via network, not information about data type.
So any manipulations with data type of response body on tests side looks strange for me.

Collapse
piczmar_0 profile image
Marcin Piczkowski Author

"Are you sure we can "unit test" a controller" - yes, you can.

"..any manipulations with data type of response body on tests side looks strange for me." - kind of true, but the thing is that because you are using MockMvc and not really the runtime config you get different JSON parser by default anyway. You can get as close as possible to your runtime config through additional hacks which I described in the post. This is a question not whether you can unit test controllers but whether you should. Integration tests have its own pros & cons.

Collapse
laptevn profile image
Nickolay Laptev • Edited

1 Within unit tests we operate only with methods of a class. Within components tests we can operate with several classes.
But we don't know anything about controllers, endpoints, threads, timers, file system in unit (and component) tests.
We know about them in integration tests.

Remember last year where we had to fight with crazy "unit tests" that run for ages :-) Lack of separation between tests was the major issue I saw in all products.
Pure unit and component tests run super fast since they operate only with RAM.

2 Let's identify what we want to test ignoring how we name this test (i.e. integration or unit test).
If we test an endpoint (web service according to target post), probably we want to verify how this endpoint reacts to incoming requests by checking what is returned as a response.
Since response body is plain text in our case this is what we want to check.
But we convert this body to some format by adding some workarounds to achieve that and just to finally verify the body itself. Why not to verify the body as is without redundant conversion to some format? :-)
So we could replace
.andExpect(jsonPath("$.value", is(ExampleController.VALUE_DOUBLE_FAIL)));
with
.andExpect(jsonPath("$.value",
is(ExampleController.VALUE_BIG_DECIMAL_FAIL.toString())));

and remove a workaround with JSON parser. Please note that this is a pseudo code (i.e. comparing string values) but the idea should be clear.
In this case we don't loose anything since we verify the only thing we care - response body.

Thread Thread
piczmar_0 profile image
Marcin Piczkowski Author • Edited
  1. I'm talking about unit tests as such that do not require whole spring context to start.

  2. That won't work because of the library limitation. I see your point but that's how the asserts works. If you try to compare "to-string" expected value with the decimal value from controller they will fail. The library would need to convert the decimals somewhow to string before the assert and this would also be a smelly hack :) In current state you'll get:

java.lang.AssertionError: JSON path "$.value"
Expected: is "0.071854545056424"
     but: was <0.071854545056424>
Expected :is "0.071854545056424"

Actual   :<0.071854545056424>
Collapse
eaziso profile image
El mehdi AZROUR

you can use the method value of jsonPathResultMatchers !
.andExpect(jsonPath("$.value").value(YOURVALUE))

?

Collapse
dreambrother profile image
Nikita Shmakov • Edited

Another solution :)

Matcher<Double> isDouble(Double d) {
    return anyOf(is(d), is(BigDecimal.valueOf(d)));
}