How to make a unit test to a @Controller class

Bassem 24/05


credit: Markus Spiske on Unsplash

In this article, we are going to see how to make a unit test to a @Controller annotated class. This means we are going to isolate it from the spring context.
Normally when we test our application with @SpringBootTest , we are making an integration test, as we are using SpringBootContextLoader as the default ContextLoader, initiating all the beans in our application.

Worth mentioning that spring framework provide also other annotations which allow us to slice the context. For example @WebMvcTest for testing the web layer, where we can choose a specific controller to be checked, @DataJpaTest for testing the JPA and the data base layer, a complete list can be found here.

So first thing, we are going to make a RestController which will present the following Post object :

public class Post {

    private String id;
    private String postTitle;
    private String body;

    public Post (String postTitle, String body) {
        this.id = UUID.randomUUID().toString();
        this.postTitle = postTitle;
        this.body = body;
    }
    // getters and setters
}

Nothing fancy there, we have three private properties and we are simulating the id generated by a random UUID.

Next we have our controller:


@RestController
public class PostController {
   
    @GetMapping("/post/{random}")
    public Post getPost(@PathVariable Integer random) {
        return new Post("postTitle "+ random, "some body");
    }


}

Our class in annotated with @RestController, which will consent us wrap the generated post in a response body automatically, that happens because the annotation it self is annotated with @ResponseBody.
Then we have our method, where we return a new post with a post title concatenated with a number exchanged throw the @PathVariable. Finally the end point is exposed throw a http get by using the @GetMapping annotation.
Next let's see our test :

@ExtendWith(SpringExtension.class)
class UnitestcontrollerApplicationTests {

    private static MockMvc mockMvc;

    @BeforeAll
    static void init () {
        mockMvc = MockMvcBuilders.standaloneSetup(PostController.class).build();
    }

    @Test
    void testController() throws Exception {
        mockMvc.perform(get("/post/1")).andDo(print()).andExpect(status().isOk());
    }
    
    @Test
    void shouldGive405ForPost() throws Exception {
        mockMvc.perform(post("/post/1")).andDo(print()).andExpect(status().isMethodNotAllowed());
    }

}

Our test class is annotated with @ExtendWith(SpringExtension.class) , in this way we are integrating the Spring TestContext Framework into JUnit 5, so we are using some overridden junit methods like beforAll, afterAll, etc.
In addition we could use the @MockBean annotation to add mocks to our context and finally we gain access to the ApplicationContext associated with the test. In fact if we take a look at the logging messages during the test, we can note that our contextInitializerClasses array is empty:

[main] DEBUG org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate - Storing ApplicationContext [1808884231] in cache under key [[MergedContextConfiguration@510f3d34 testClass = UnitestcontrollerApplicationTests, locations = '{}', classes = '{}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{}', 

To read more about caching the application context, take a look at the official docs here.

Next we have the MockMvc object, the main tool to test our controller class, it's a static variable, so we can initiate it in the @BeforeAll method:

private static MockMvc mockMvc;

@BeforeAll
static void init () {
        mockMvc = MockMvcBuilders.standaloneSetup(PostController.class).build();
    }

We are making a custom build for our mockMvc object with MockMvcBuilders, so we can start our test with the minimum infrastructure required and we are also testing just one controller (can accept also an array).

Next, let's take a look at our tests:

@Test
    void testController() throws Exception {
        mockMvc.perform(get("/post/1")).andDo(print()).andExpect(status().isOk());
    }
    
    @Test
    void shouldGive405ForPost() throws Exception {
        mockMvc.perform(post("/post/1")).andDo(print()).andExpect(status().isMethodNotAllowed());
    }

Nothing fancy there, we are making sure that our end point is working with first test and that is accepting only get requests with the second one.
Now let's us introduce a complication our controller by injecting a service, which will be responsible for generating the new post, the result will be as follow:

@RestController
public class PostController {

    private final PostService service;

    public PostController(PostService service) {
        this.service = service;
    }
    @GetMapping("/post/{random}")
    public Post getPost(@PathVariable Integer random) {
        return service.getAPost(random);
    }
}

So we need to change our test to integrate the new modification, as following:

@ExtendWith(SpringExtension.class)
class UnitestcontrollerApplicationTests {

    private static MockMvc mockMvc;
    private static PostService service = mock(PostService.class);

    @BeforeAll
    static void init () {
        mockMvc = MockMvcBuilders.standaloneSetup(new PostController(service)).build();
    }

    @Test
    void testController() throws Exception {
        when(service.getAPost(1)).thenReturn(new Post("postTitle", "body"));
        mockMvc.perform(get("/post/1")).andDo(print()).andExpect(status().isOk());
    }
    
    @Test
    void shouldGive405ForPost() throws Exception {
        when(service.getAPost(1)).thenReturn(new Post("postTitle", "body"));
        mockMvc.perform(post("/post/1")).andDo(print()).andExpect(status().isMethodNotAllowed());
    }

}

As you can see, we mocked the postService using static method mock() of Mockito, we could also use @MockBean, as we are using the SpringExtension.class. Then we constructed our controller object using the mock just created. Also in each test we are returning a mock response using when(..).thenReturn(..) statement of Mockito.

Finally, if we want to get rid of all the Spring dependencies and use only Mockito (as our test) , we have only to remove the @ExtendWith(SpringExtension.class). In this way we have a true junit test.

That's it; all the code written in this post can be found on GitHub.



Feel free to join the conversation on Twitter 👇