How to Write MockMvc Tests Without ObjectMapper, Part Three - Should We Do It?

A month ago I found a blog post which argues that we shouldn't build the request body with ObjectMapper when we are writing integration tests with MockMvc (aka Spring MVC Test framework). Because I think that the original blog post is quite interesting, I decided to write a blog post that evaluates if we should get rid of ObjectMapper.

After we have finished this blog post, we:

  • Understand what's the role of ObjectMapper.
  • Can identify the pros and cons of not using ObjectMapper.
  • Know if we should get rid of ObjectMapper.

The Role of ObjectMapper

Before we can understand the pros and cons of the solutions which I introduced in the first and second part of this article series, we have to take a closer look at the role of ObjectMapper. We can use an ObjectMapper object for two purposes:

  1. It can serialize an object into a JSON document. If we write a test which generates a request body with ObjectMapper, our test uses it for this purpose. Also, the system under test uses ObjectMapper for this purpose when it generates the JSON document that's added to the body of the returned HTTP response.
  2. It can deserialize a JSON document into an object. The system under test uses ObjectMapper for this purpose when it transforms the request body into a data transfer object.

The following figure illustrates the role of ObjectMapper:

The role of ObjectMapper when we write tests with MockMvc

When we write tests which use ObjectMapper, we can use one of these three configurations:

1. Test classes and the system under test use the same ObjectMapper object.

If we use this configuration, the same ObjectMapper object that serializes an object into a JSON document deserializes the JSON document into a data transfer object that's processed by the system under test. We will use this configuration if:

  • We configure MockMVC (aka Spring MVC Test framework) by using the application context based configuration, and we inject an ObjectMapper bean into our test class and use this ObjectMapper object for creating the JSON documents which are send to the system under test.
  • We configure MockMVC (aka Spring MVC Test framework) by using the standalone configuration and:
    • We configure a custom HttpMessageConverter which can read and write JSON by using Jackson (aka the MappingJackson2HttpMessageConverter) and
    • The created MappingJackson2HttpMessageConverter uses the same ObjectMapper object that's used to generate the JSON documents which are send to the system under test.

The following figure illustrates the responsibilities of the shared ObjectMapper object:

The responsibilities of a shared ObjectMapper object

2. Test classes and the system under test use different ObjectMapper objects which share the same configuration.

If we use this configuration, serialization and deserialization is done by different ObjectMapper objects, but these objects behave the same way because they use the same configuration. We will use this configuration if we have one method that creates and configures a new ObjectMapper object, and the system under test and our tests use this method. The following figure illustrates the responsibilities of these ObjectMapper objects:

The responsibilies of ObjectMapper objects which use the same configuration

3. Test classes and the system under test use different ObjectMapper objects which don't share any configuration.

If we use this configuration, serialization and deserialization is done by different ObjectMapper objects which might or might not behave the same way. We will use this configuration if the ObjectMapper objects used by the system under test and our tests aren't created or configured by the same code. The following figure illustrates the responsibilities of these ObjectMapper objects:

The responsibilities of totally ObjectMapper objects which are not created or configured by the same code

We should now understand how we can use ObjectMapper when we are writing tests with MockMVC (aka Spring MVC Test framework). Next, we will take a look at the pros and cons of not using the ObjectMapper.

The Pros and Cons of Not Using ObjectMapper

Before we can decide if we should use ObjectMapper when we build the request body that's send to the system under test, we must identify the pros and cons of alternative solutions. This section helps us to do just that.

The pros of not using ObjectMapper are:

1. We have full control over the request body that's send to the system under test. If we build the request body with ObjectMapper, we cannot control:

  • The structure of the JSON document because that's specified by the data transfer object which is passed to the ObjectMapper.
  • The formats which are used by the property values because the serialization behavior of an ObjectMapper object depends on its configuration.

This means that we cannot write tests which ensure that the system under test is working as expected when:

  • The request body is missing a property.
  • The request body contains unknown properties.
  • The property values use unsupported format. For example, we cannot write tests which use unsupported date or timestamp formats.

On the other hand, if we don't use ObjectMapper when we build the request body that's send to the system under test, we don't have these problems because we have full control over the structure of the JSON document and the formats used by the property values.

To be fair, I have to admit that we can solve these problems even if we build our request body with ObjectMapper.

If we want to write tests which ensure that the system under test is working as expected when the request body is missing a property, we have create a duplicate DTO class which contains the same properties as the "original" class and doesn't contain the missing property. After we have created a new DTO class, we have to use the created class when we want to create the request body with ObjectMapper. However, I wouldn't use this technique because it makes our tests hard to maintain.

If we want to write tests which ensure that the system under test is working as expected when the request body contains unknown properties, we can either:

  1. Create a duplicate DTO class which contains all properties of the "original" class and all unknown properties.
  2. Create a new DTO class which extends the "original" DTO class and declares the required unknown properties.

After we have created a new DTO class, we have to use the created class when we want to create the request body with ObjectMapper. However, I wouldn't use this technique because the first option makes our tests hard to maintain and the second options adds unnecessary complexity to our test suite.

If we want to write tests which ensure that the system under test is working as expected when the property values use unsupported format, we have to create a new ObjectMapper object and ensure that it uses the unsupported format when it serializes the data transfer object. Even though this works, the downside of this approach is that managing the different ObjectMapper configurations adds unnecessary complexity to our test suite.

2. It's possible/easier to write tests that can fail. If we build the request body with ObjectMapper and we configure the ObjectMapper by using either the option one or two, we cannot write tests which can fail because the serialization and deserialization behavior depends on the configuration of the ObjectMapper, and the system under test and our tests use the same configuration.

On the other hand, if we don't create the request body with ObjectMapper, it's easier to write tests which can fail because we have full control over the request body that's send to the system under test. For example, if we change the supported date or timestamp format, the tests which use the old format will fail.

If we configure ObjectMapper by using the option three, we can write tests which can fail because the ObjectMapper objects used by the system under test and our tests aren't created or configured by the same code. In other words, if we change the configuration of the ObjectMapper that's used by the system under test, the behavior of the ObjectMapper that's used by our tests won't change and our tests will fail.

3. It's easy to see what kind of a JSON document is send to the system under test. If we build the request body with ObjectMapper and we want to take a look at the JSON document that's send to the system under test, we have to run our test and use a debugger or invoke the print() method of the MockMvcResultHandlers class. Although these options aren't super complicated, it's a lot easier to simply take a look at the string that contains the request body or take a look at the template that's used to build the actual request body.

If we build the request body by using a template and we want to see the actual request body (including property values), we must use a debugger or invoke the print() method of the MockMvcResultHandlers class. That being said, I have noticed that seeing the request body template is often extremely helpful.

4. We can write more comprehensive tests for the deserialization logic. If we don't build the request body with ObjectMapper, our tests might catch deserialization bugs which don't occur if we build the request body with ObjectMapper. Also, philosophically speaking, I think that our tests are "better" because the input data (aka JSON document) isn't produced by the same library which consumes it.

The cons of not using ObjectMapper are:

1. We might write code that's hard to write and maintain. If we specify the required request bodies by using static strings, we often have multiple different versions of the same JSON document. In other words, we have to declare one constant per version. This makes our tests hard to write. Also, if we have to make changes to our test data, we have to make the required changes to multiple constants. This means that our tests tests are hard to maintain.

2. We might have to write code that isn't easy to read and write. If we specify the required request bodies by using string concatenation, our test code looks awful and is hard to write. On the other hand, Because the format() method of the String class doesn't support named arguments, it's easy to make a programming error. In other words, because we have to be extra careful and ensure that our code builds the "correct" request body, using this method is somewhat slow.

3. We might have to write more custom code. If we build the required request bodies by using a template engine, we have to write custom code that must be maintained in the future. This will take more time than maintaining the test code which builds the required request bodies with ObjectMapper.

We are now familiar with the pros and cons of not using ObjectMapper. Let's move on and find out if we should get rid of ObjectMapper.

Should We Get Rid of ObjectMapper?

I would love to say: absolutely because controversy would most likely bring me a lot of readers, but the truth is that it depends. When we make this decision, we should take the following things into account:

1. What's our risk tolerance level? If our risk tolerance level is low, we have to try to eliminate risks which we can ignore if our risk tolerance level is high. In other words, if our risk tolerance level is low, we have to write more (and better) automated tests. If we are in this situation, it makes sense to replace ObjectMapper with a tool that helps us to write more comprehensive tests.

2. How many tests do we already have? If we have a lot of tests which generate the input data with ObjectMapper, we might not be able to get rid of ObjectMapper because we don't have time to refactor our existing tests. There are situations when being consistent is more important than using the best approach.

3. Should we find a compromise? I think that one reason why people want to use ObjectMapper for generating the input data is that it's so easy. That's is why it can be hard to convince these people to adopt a new technique that requires more work (from their point of view). Nevertheless, if we want to write tests which require input data that cannot be generated with ObjectMapper, it could be wise to find a compromise where:

  • Most of the tests would use ObjectMapper for building the input data that's send to the system under test.
  • The tests which cannot use ObjectMapper would use something else like a static string, a dynamic string, or a template engine.

4. What's our preference? Everyone of us has our own preferences (sometimes they are called unconscious biases) which will cloud our judgment unless we are aware of them and take them into account when we make decisions like this. For example, if I start to feel uncomfortable or maybe even a bit angry when I am reading an article or discussing about something, I know that the article or the other person is making a good point and I am reacting to it because I want to stay on my comfort zone and it's not possible if I admit that I am wrong. In other words, if the idea of getting rid of ObjectMapper makes you feel uncomfortable, you should try to figure out why you react in this way (and perhaps get rid of ObjectMapper).

We can now identify the things which we should take into account when we decide if we should ditch ObjectMapper. Let's summarize what we learned from this blog post.

Summary

This blog post has taught us seven things:

  • An ObjectMapper object can serialize an object into a JSON document and deserialize a JSON document into an object.
  • If we don't use ObjectMapper, we have full control over the request body that's send to the system under test. This gives us the possibility to write more comprehensive tests that can fail.
  • If we replace ObjectMapper with a static string, our tests will be hard to write and maintain.
  • If we replace ObjectMapper with a dynamic string, our tests will be hard to read and write.
  • If we replace ObjectMapper with a template engine, we have to write custom code that must be maintained in the future.
  • If our risk tolerance level is low, we shouldn't use ObjectMapper when we generate the JSON document that's send to the system under test.
  • Sometimes it's wise to find a compromise. In other words, we can use ObjectMapper as long as we are ready to use something else (a static string, a dynamic string, or a template engine) when we have to write tests which require input data that cannot be generated with ObjectMapper.
0 comments… add one

Leave a Reply