Mocks and stubs

If we wanted to test the search request handled by the SearchController class, we would certainly want to mock SearchService.

There are two ways of doing this: with a mock or with a stub.

Mocking with Mockito

First, we can create a mock object with Mockito:

package masterSpringMvc.search;

import masterSpringMvc.MasterSpringMvcApplication;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import java.util.Arrays;

import static org.hamcrest.Matchers.*;
import static org.mockito.Matchers.*;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = MasterSpringMvcApplication.class)
@WebAppConfiguration
public class SearchControllerMockTest {
    @Mock
    private SearchService searchService;

    @InjectMocks
    private SearchController searchController;

    private MockMvc mockMvc;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
        this.mockMvc = MockMvcBuilders
                .standaloneSetup(searchController)
                .setRemoveSemicolonContent(false)
                .build();
    }

    @Test
    public void should_search() throws Exception {

        when(searchService.search(anyString(), anyListOf(String.class)))
                .thenReturn(Arrays.asList(
                        new LightTweet("tweetText")
                ));

        this.mockMvc.perform(get("/search/mixed;keywords=spring"))
                .andExpect(status().isOk())
                .andExpect(view().name("resultPage"))
                .andExpect(model().attribute("tweets", everyItem(
                        hasProperty("text", is("tweetText"))
                )));

        verify(searchService, times(1)).search(anyString(), anyListOf(String.class));
    }
}

You can see that instead of setting up MockMvc with the web application context, we have created a standalone context. This context will only contain our controller. That means we have full control over the instantiation and initialization of controllers and their dependencies. It will allow us to easily inject a mock inside of our controller.

The downside is that we have to redeclare pieces of our configuration like the one saying we don't want to remove URL characters after a semicolon.

We use a couple of Hamcrest matchers to assert the properties that will end up in the view model.

The mocking approach has its benefits, such as the ability to verify interactions with the mock and create expectations at runtime.

This will also couple your test with the actual implementation of the object. For instance, if you changed how a tweet is fetched in the controller, you would likely break the tests related to this controller because they still try to mock the service we no longer rely on.

Stubbing our beans while testing

Another approach is to replace the implementation of our SearchService class with another one in our test.

We were a bit lazy early on and did not define an interface for SearchService. Always program to an interface and not to an implementation. Behind this proverbial wisdom lies the most important lesson from the Gang of Four.

One of the benefits of the Inversion of Control is to allow for the easy replacement of our implementations in tests or in a real system. For this to work, we will have to modify all the usages SearchService with the new interface. With a good IDE, there is a refactoring called extract interface that will do just that. This should create an interface that contains the public method search() of our SearchService class:

public interface TwitterSearch {
    List<LightTweet> search(String searchType, List<String> keywords);
}

Of course, our two controllers, SearchController and SearchApiController, must now use the interface and not the implementation.

We now have the ability to create a test double for the TwitterSearch class specially for our test case. For this to work, we will need to declare a new Spring configuration named StubTwitterSearchConfig that will contain another implementation for TwitterSearch. I placed it in the search package, next to SearchControllerMockTest:

package masterSpringMvc.search;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import java.util.Arrays;

@Configuration
public class StubTwitterSearchConfig {
    @Primary @Bean
    public TwitterSearch twitterSearch() {
        return (searchType, keywords) -> Arrays.asList(
                new LightTweet("tweetText"),
                new LightTweet("secondTweet")
        );
    }
}

In this configuration class, we redeclare the TwitterSearch bean with the @Primary annotation, which will tell Spring to use this implementation on priority if other implementations are found in the classpath.

Since the TwitterSearch interface contains only one method, we can implement it with a lambda expression.

Here is the complete test that uses our StubConfiguration class along with our main configuration with the SpringApplicationConfiguration annotation:

package masterSpringMvc.search;

import masterSpringMvc.MasterSpringMvcApplication;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.hamcrest.Matchers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = {
        MasterSpringMvcApplication.class,
        StubTwitterSearchConfig.class
})
@WebAppConfiguration
public class SearchControllerTest {
    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;

    @Before
    public void setup() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
    }

    @Test
    public void should_search() throws Exception {

        this.mockMvc.perform(get("/search/mixed;keywords=spring"))
                .andExpect(status().isOk())
                .andExpect(view().name("resultPage"))
                .andExpect(model().attribute("tweets", hasSize(2)))
                .andExpect(model().attribute("tweets",
                                hasItems(
                                        hasProperty("text", is("tweetText")),
                                        hasProperty("text", is("secondTweet"))
                                ))
                );
    }
}

Should I use mocks or stubs?

Both approaches have their own merits. For a detailed explanation, check out this great essay by Martin Fowler: http://martinfowler.com/articles/mocksArentStubs.html.

My testing routine is more about writing stubs because I like the idea of testing the output of my objects more than their inner workings. But that's up to you. Spring being a dependency injection framework at its core means that you can easily choose what your favorite approach is.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset