Yesterday I found a blog post which argues that we shouldn't build the request body with an 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 describes how we can build the request body that's send to the system under test if we cannot use ObjectMapper
, we must use the simplest possible solution, and we must use Java.
After we have finished this blog post, we:
- Can identify the different options which we can use when we want to build the request body that's send to the system under test.
- Understand the pros and cons of each option.
- Can select the best option.
Let's begin.
Building the Request
This blog post assumes that we are writing a video game catalog software and we must write tests for a REST API endpoint which inserts the information of a new game into the database. When we want to create a new game, we have to send a POST
request to that API endpoint and add the following JSON document to the request body:
{ "categoryId": 1, "description": "Amiga version.", "gameVersionId": "9a689283-fd3b-46a8-b371-80b412455bb7", "hasBox": true, "hasGameMedia": true, "hasManual": true, "platformId": 1, "title": "Wayne Gretzky Hockey" }
When we build the request that's send to the system under test, we will use a so called request builder class because it will help us to get rid of the duplicate code that's often found from tests which use MockMvc
. Our request builder class has one method called create()
that takes the request body as a method parameter. This method builds the request and sends the created request to the system under test.
The relevant part of our request builder class looks as follows:
import org.springframework.test.web.servlet.ResultActions public class GameApiRequestBuilder { public ResultActions create(String requestBody) throws Exception { //Implementation is omitted on purpose } }
- Validation fails.
- The request body contains unknown JSON properties.
- Valid information is provided to the system under test.
Next, we will try to build the request body by using the simplest possible option.
Option 1: A String Literal
If we want to use the simplest possible option, we can pass a string literal to the create()
method of the GameApiRequestBuilder
class. If we are using Java 14 or older, our test class could look as follows:
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @DisplayName("Tests for the game API") class GameApiTest { private final GameApiRequestBuilder requestBuilder = new GameApiRequestBuilder(); @Nested @DisplayName("Create a new game") class CreateNewGame { @Test @DisplayName("Should return X") void shouldReturnX() throws Exception { var response = requestBuilder.create("{" + "\"categoryId\": 1," + "\"description\": \"Amiga version.\"," + "\"gameVersionId\": \"9a689283-fd3b-46a8-b371-80b412455bb7\"," + "\"hasBox\": true," + "\"hasGameMedia\": true," + "\"hasManual\": true," + "\"platformId\": 1," + "\"title\": \"Wayne Gretzky Hockey\"" + "}" ); //Write assertions for the response } } }
If we are using Java 15 or newer, we can use a feature called text blocks which makes our life a lot easier when we have to add multiline strings to our Java code. If we replace our old fashioned multiline string with a text block, the source code of our test class looks as follows:
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @DisplayName("Tests for the game API") class GameApiTest { private final GameApiRequestBuilder requestBuilder = new GameApiRequestBuilder(); @Nested @DisplayName("Create a new game") class CreateNewGame { @Test @DisplayName("Should return X") void shouldReturnX() throws Exception { var response = requestBuilder.create(""" { "categoryId": 1, "description": "Amiga Version.", "gameVersionId": "9a689283-fd3b-46a8-b371-80b412455bb7", "hasBox": true, "hasGameMedia": true, "hasManual": true, "platformId": 1, "title": "Wayne Gretzky Hockey" } """ ); //Write assertions for the response } } }
The pros of this option are:
- It's very easy to use a string literal for this purpose.
- If we can use Java 15 or newer, we can use text blocks which means that our string literal is extremely easy to write and read.
The cons of this option are:
- If we must use Java 14 or older, string literals are a bit cumbersome to write. Also, they do look a bit ugly.
- If we have to write multiple tests for the system under test, we have to copy the string literal to every test method which needs it. This makes our tests hard to maintain.
Let's move on and find out if we can fix our copy-and-paste code by using constants.
Option 2: A Hard-Coded Constant
We can remove our copy-and-paste code by following these steps:
- Declare a constant that contains the request body.
- Pass the constant to the
create()
method of theGameApiRequestBuilder
class.
- If the test methods which use our constant are found from one test class, we should naturally put our constant to that test class.
- If the test methods which use our constant are found from multiple test classes, we should create a constant class (a class that contains only constants) and put our constant to that class.
If we are using Java 14 or older, our test class looks as follows:
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @DisplayName("Tests for the game API") class GameApiTest { private final GameApiRequestBuilder requestBuilder = new GameApiRequestBuilder(); @Nested @DisplayName("Create a new game") class CreateNewGame { private final String REQUEST_BODY = "{" + "\"categoryId\": 1," + "\"description\": \"Amiga version.\"," + "\"gameVersionId\": \"9a689283-fd3b-46a8-b371-80b412455bb7\"," + "\"hasBox\": true," + "\"hasGameMedia\": true," + "\"hasManual\": true," + "\"platformId\": 1," + "\"title\": \"Wayne Gretzky Hockey\"" + "}"; @Test @DisplayName("Should return X") void shouldReturnX() throws Exception { var response = requestBuilder.create(REQUEST_BODY); //Write assertions for the response } } }
If we are using Java 15 or newer, our test class looks as follows:
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @DisplayName("Tests for the game API") class GameApiTest { private final GameApiRequestBuilder requestBuilder = new GameApiRequestBuilder(); @Nested @DisplayName("Create a new game") class CreateNewGame { private final String REQUEST_BODY = """ { "categoryId": 1, "description": "Amiga Version.", "gameVersionId": "9a689283-fd3b-46a8-b371-80b412455bb7", "hasBox": true, "hasGameMedia": true, "hasManual": true, "platformId": 1, "title": "Wayne Gretzky Hockey" } """; @Test @DisplayName("Should return X") void shouldReturnX() throws Exception { var response = requestBuilder.create(REQUEST_BODY); //Write assertions for the response } } }
The pros of this option are:
- If we have to change the request body, we have to make this change only to one place. This makes our tests easy to maintain.
- If we can use Java 15 or newer, we can use text blocks which means that our constant is extremely easy to write and read.
The cons of this option are:
- If we must use Java 14 or older, defining the value of our constant is a bit cumbersome. Also, our constant looks a bit ugly.
- If we have to write multiple tests which use different test data (either different property values or a totally different JSON documents), we have to declare one constant per request body. 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.
Next, we will get rid of duplicate test data by building a dynamic string.
Option 3: A Dynamic String
The simplest way to build a dynamic string is to use string concatenation. After we have declared the constants which contain the property values of our JSON document and created the request body by using string concatenation, the source code of our test class looks as follows:
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @DisplayName("Tests for the game API") class GameApiTest { private final GameApiRequestBuilder requestBuilder = new GameApiRequestBuilder(); @Nested @DisplayName("Create a new game") class CreateNewGame { private final int CATEGORY_ID = 1; private final String DESCRIPTION = "Amiga version."; private final String GAME_VERSION_ID = "9a689283-fd3b-46a8-b371-80b412455bb7"; private final boolean HAS_BOX = true; private final boolean HAS_GAME_MEDIA = true; private final boolean HAS_MANUAL = true; private final int PLATFORM_ID = 1; private final String TITLE = "Wayne Gretzky Hockey"; private final String REQUEST_BODY = "{" + "\"categoryId\": " + CATEGORY_ID + "," + "\"description\": \"" + DESCRIPTION + "\"," + "\"gameVersionId\": \"" + GAME_VERSION_ID + "\"," + "\"hasBox\": " + HAS_BOX + "," + "\"hasGameMedia\": " + HAS_GAME_MEDIA + "," + "\"hasManual\": " + HAS_MANUAL + "," + "\"platformId\": " + PLATFORM_ID + "," + "\"title\": \"" + TITLE + "\"" + "}"; @Test @DisplayName("Should return X") void shouldReturnX() throws Exception { var response = requestBuilder.create(REQUEST_BODY); //Write assertions for the response } } }
Even though string concatenation is quite simple, I think that the code which uses it is hard to write and looks awful. Let's make our code a bit better by using the format()
method of the String
class.
If we are using Java 14 or older, our test class looks as follows:
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @DisplayName("Tests for the game API") class GameApiTest { private final GameApiRequestBuilder requestBuilder = new GameApiRequestBuilder(); @Nested @DisplayName("Create a new game") class CreateNewGame { private final int CATEGORY_ID = 1; private final String DESCRIPTION = "Amiga version."; private final String GAME_VERSION_ID = "9a689283-fd3b-46a8-b371-80b412455bb7"; private final boolean HAS_BOX = true; private final boolean HAS_GAME_MEDIA = true; private final boolean HAS_MANUAL = true; private final int PLATFORM_ID = 1; private final String TITLE = "Wayne Gretzky Hockey"; private final String REQUEST_BODY = String.format( "{" + "\"categoryId\": %d," + "\"description\": \"%s\"," + "\"gameVersionId\": \"%s\"," + "\"hasBox\": %s," + "\"hasGameMedia\": %s," + "\"hasManual\": %s," + "\"platformId\": %d," + "\"title\": \"%s\"" + "}", CATEGORY_ID, DESCRIPTION, GAME_VERSION_ID, HAS_BOX, HAS_GAME_MEDIA, HAS_MANUAL, PLATFORM_ID, TITLE ); @Test @DisplayName("Should return X") void shouldReturnX() throws Exception { var response = requestBuilder.create(REQUEST_BODY); //Write assertions for the response } } }
If we are using Java 15 or newer, our test class looks as follows:
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @DisplayName("Tests for the game API") class GameApiTest { private final GameApiRequestBuilder requestBuilder = new GameApiRequestBuilder(); @Nested @DisplayName("Create a new game") class CreateNewGame { private final int CATEGORY_ID = 1; private final String DESCRIPTION = "Amiga version."; private final String GAME_VERSION_ID = "9a689283-fd3b-46a8-b371-80b412455bb7"; private final boolean HAS_BOX = true; private final boolean HAS_GAME_MEDIA = true; private final boolean HAS_MANUAL = true; private final int PLATFORM_ID = 1; private final String TITLE = "Wayne Gretzky Hockey"; private final String REQUEST_BODY = String.format( """ { "categoryId": %d, "description": "%s", "gameVersionId": "%s", "hasBox": %s, "hasGameMedia": %s, "hasManual": %s, "platformId": %d, "title": "%s" } """, CATEGORY_ID, DESCRIPTION, GAME_VERSION_ID, HAS_BOX, HAS_GAME_MEDIA, HAS_MANUAL, PLATFORM_ID, TITLE ); @Test @DisplayName("Should return X") void shouldReturnX() throws Exception { var response = requestBuilder.create(REQUEST_BODY); //Write assertions for the response } } }
The pros of this option are:
- It works. Also, if we move the code that creates our request body to a factory method or to a builder class, we can build real dynamic strings.
- The code that uses the
format()
method of theString
class is relatively easy to read (especially if we can use text blocks).
The cons of this option are:
- The code that uses string concatenation is both hard to write and read.
- Because the
format()
method of theString
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.
We have now identified three simple options which we can use when we have to build the request body that's send to the system under test. Also, we have evaluated the pros and cons of each option. Let's summarize what we learned from this blog post.
Summary
This blog post has taught us four things:
- We should never use a string literal unless we know that we will write only one test or we are writing a sample code for a blog post, presentation, or a video.
- If we are using Java 15 or newer, we should use text blocks because they make our code easier to write and read.
- A hard-coded constant is a good option if our tests require only a handful of different request bodies.
- A dynamic string is a good option if our tests require more than a handful of different request bodies. However, I think that if we need this kind of flexibility, we should use a template engine.