lukas-krecan / JsonUnit

Compare JSON in your Unit Tests
Apache License 2.0
871 stars 115 forks source link

ObjectMapper customization by autowiring from Spring #535

Open dominikbrandon opened 1 year ago

dominikbrandon commented 1 year ago

Summary I've got a case where I need to customize the ObjectMapper instance used by JsonUnit, moreover, I don't want to create a new instance of it, but rather use the one provided by Spring context instead.

Solution I'd like A way to set up JsonUnit's ObjectMapper programmatically instead of via SPI, e.g.

@Configuration
class JsonUnitConfiguration {

    @Bean
    <json unit config class> jsonUnit(ObjectMapper objectMapper) {
        return new <json unit config class>(objectMapper);
    }
}

Do you think something similar would be possible to implement?

Alternative solution I've come up with a workaround for that, but it's not too good, as there might appear an NPE when JsonUnit tries to access ObjectMapper before Spring autowires it.

import com.fasterxml.jackson.databind.ObjectMapper
import net.javacrumbs.jsonunit.providers.Jackson2ObjectMapperProvider
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component

@Component
class JsonUnitObjectMapperProvider implements Jackson2ObjectMapperProvider {

    private static ObjectMapper objectMapper

    JsonUnitObjectMapperProvider() {
    }

    @Autowired
    JsonUnitObjectMapperProvider(ObjectMapper mapper) {
        objectMapper = mapper
    }

    @Override
    ObjectMapper getObjectMapper(boolean lenient) {
        return objectMapper
    }
}
lukas-krecan commented 1 year ago

Hi, thanks for feedback. This is something that's on my mind for years. All JsonUnit assertion methods are basically static or have a static entry-point. This does not play well with the dynamic nature of Spring. There are two out of this

  1. Expose Spring initialized ObjectMapper on some statis variable and access it from Jackson2ObjectMapperProvider. This is a bit brittle, but should work with usual Spring test setup where you ensure that Spring is initialized before the test.
  2. Expose an entry point to JsonUnit that would allow you to inject the ObjectMapper. This would require me to refactor the library and it will be harder to use. You would have to instantiate and configure an assertion class. I do not have the capacity to do it and I am not sure it would be worth the effort.
dominikbrandon commented 1 year ago

I see, thanks for the response. What about kind of mixing those two? I mean some static function that would allow you to provide an ObjectMapper instance and then JsonUnit would internally store it in some static field. In my opinion, it would be quite easy to use - it would allow you to call this static function inside the setup method, which would be easy for both: creating and configuring your own instance manually, or using the one autowired by Spring. I believe some people would still prefer to not worry about ObjectMapper at all, so there could be a default one. What do you think about that? The difficulty here would be to provide backward compatibility for providing ObjectMapper using SPI, but I believe this will be possible to achieve in some way.

In case the description is unclear, I mean something like this (it's Groovy+Spock):

abstract class IntegrationSpec extends Specification {

    @Autowired
    ObjectMapper objectMapper

    def setupSpec() {
        JsonUnitNewConfigClass.injectObjectMapper(objectMapper)
    }
}
lukas-krecan commented 1 year ago

Yes, that would be possible. I am reluctant to add this as it is quite brittle and I do not want to be solving issues of people using it incorrectly. You can do the same using Jackson2ObjectMapperProvider

dominikbrandon commented 1 year ago

Why do you consider this solution brittle? I feel like it's a clear and explicit way of providing ObjectMapper instance and I'm just curious what exactly makes you feel that it's the wrong approach.

And regarding the point that you can do the same using Jackson2ObjectMapperProvider - yes, you're right. I've made it this way and it seems like it's working seamlessly. But doing so made me feel like I'm hacking it - I was particularly worried about a possible NullPointerException in the case of JsonUnit trying to use ObjectMapper before it's been autowired. Yes, it should have been because usually you first run the application which makes the whole Spring context start, then you execute some actions, and finally, you assert using JsonUnit. So assuming that ObjectMapper gets loaded from Jackson2ObjectMapperProvider only when one of the JsonUnit's methods gets called, this would, and does, work. But I didn't know when exactly JsonUnit does that - maybe it's at the start of the execution, or right after that.

Anyway, maybe a cheap and good enough solution would be to put a hint in the docs on how the ObjectMapper may be provided when using Spring, what are your thoughts about that? Maybe this will be helpful for future users of the library.

lukas-krecan commented 1 week ago

Just a note for my future self. I tried to implement it and the trouble is that AssertJ style tests are currently parsing the value before any configuration can happen -assertThat(json) is parsing the value before any other method can be called.

lukas-krecan commented 1 week ago

Workaround - instead of string, you can provide Jackson JsonNode. Instead of assertThat(jsonString).isEqualTo(jsonString2) you can do assertThat(m.readTree(jsonString)).isEqualTo(m.readTree(jsonString2))