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.
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.
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")) )) ); } }
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.