stevelibonati

February 5, 2012

spring-mvc-test with a sprinkle of mockito

Filed under: spring, spring-mvc, unit test — Tags: , , , , , , , , — stevelibonati @ 1:44 pm

Sam Brannen and Rossen Stoyanchev presented ‘Spring 3.1 and MVC Testing Support’ at Spring 2GX back in October 2011. The idea is that you can write a unit test that accepts a Spring app context defining your Spring MVC components and be able to test your Controllers and all the Spring MVC ‘wiring’ by passing a mock request. The test itself does not have a reference to a Controller and thus you can simply send a request and receive a response. This type of test is very powerful in that you are testing Spring configuration, @RequestMapping annotations, controller behavior and view resolution.

Up to this point, we’ve tested Controllers only as plain vanilla unit tests which of course Spring recommends. Be sure to take a look at section 10.2 of http://static.springsource.org/spring/docs/3.1.x/spring-framework-reference/html/testing.html. This typically means instantiating the controller and invoking its methods directly, asserting on the response object and injecting mock objects for any dependencies. This is good but for the webapp under consideration we leave out all the view resolution which primarily consists of jaxb marshalling which can be quite particular when you mess up the xml annotations.

To supplement this style of testing and to test the app ‘end to end’, something Sam and Rossen alluded to in their presentation, my team has built a SoapUI test suite that would act as a client to the application (a REST app in this case). Of course the app needs to be built, deployed and running. Any integrated components like databases, web services, etc need to be up as well. The SoapUI tests are definitely a solid approach for testing your web app end to end but wouldn’t there be a lot of value in leveraging the Spring MVC Test Support?

Not only can I test the Controller behavior but whereas before I was asserting only on a response object, now I can assert on the actual response. Again, in this case, we’re talking xml documents (but obviously this can be applied to any response format or content type, json, html, etc …). There was one problem with this that I quickly realized when I started playing with this test framework. The framework attempts to resolve all @Autowired dependencies. What does this mean? Well, if you value a nice decoupled app layer architecture (and I know you do), your Controllers will have Services injected into them and your Services will have DAO’s injected into them. In our case, we have DAO objects that integrate with a database as well as SOAP web services. So I want to test my Controllers but I don’t want an end to end ‘integration test’ that tests the app through all its dependencies. That’s a bit too heavy and would be a nightmare for the continuous integration.

So what I need is to leverage the Spring MVC Test support framework but figure out how to short circuit those @Autowired dependencies and inject a mock in which I can control the behavior. In the end, it was a really simple thing to achieve but I will admit my colleague and I tossed it around a bit before figuring out the solution.

So what was the magic that we needed? We recognized the simple fact that in the tests we don’t have a reference to the Controller, hence, you can’t directly inject a mocked Service object. We also have to come up with a way for Spring not to chase down all those @Autowired dependencies.

Lets consider each separately though they are related. First off, how do I get a Mockito mock object into the app context? A little googling led to this post, injecting-mockito-mocks-into-a-spring-bean. So something like the following works:

<bean id="bookService" class="org.mockito.Mockito" factory-method="mock">
<constructor-arg value="com.test.service.book.BookService" />
</bean>

Interestingly, the post doesn’t discuss how to define behavior on the mock object which is something I quickly realized needed to be accomplished. Take a look at the following :

package com.test.controller.book;

import static org.hamcrest.Matchers.equalTo;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.server.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.server.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.server.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.server.result.MockMvcResultMatchers.xpath;

import java.net.URI;
import java.util.ArrayList;
import java.util.List;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.web.server.MockMvc;
import org.springframework.test.web.server.RequestBuilder;
import org.springframework.test.web.server.request.DefaultRequestBuilder;
import org.springframework.test.web.server.samples.context.GenericWebXmlContextLoader;
import org.springframework.test.web.server.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import com.test.bean.Book;
import com.test.bean.Books;
import com.test.service.book.BookService;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader = TestGenericWebXmlContextLoader.class, locations = {
		"classpath:applicationContext.xml", "classpath:mockitoTestContext.xml" })
public class BookControllerTestContextTests {

	@Autowired
	private WebApplicationContext wac;

	private MockMvc mockMvc;

	private BookService bookService;

	@Before
	public void setup() {

		this.mockMvc = MockMvcBuilders.webApplicationContextSetup(this.wac)
				.build();

		bookService = wac.getBean(BookService.class);

		Books books = new Books();

		List bookList = new ArrayList();

		Book book2 = new Book();
		book2.setIsbn("0307408841");
		book2.setTitle("In the Garden of Beasts: Love, Terror, and an American Family in Hitler's Berlin");

		bookList.add(book2);

		books.setBooks(bookList);
		when(bookService.getBooks()).thenReturn(books);
	}

	@Test
	public void testGetBooks() throws Exception {
	

		mockMvc.perform(get("/books").header("foo","bar"))
				.andExpect(status().isOk())
				.andExpect(content().type(MediaType.APPLICATION_XML))
				.andExpect(
						xpath("/books/books/title")
								.string(equalTo("In the Garden of Beasts: Love, Terror, and an American Family in Hitler's Berlin")));
	}

}

class TestGenericWebXmlContextLoader extends GenericWebXmlContextLoader {

	public TestGenericWebXmlContextLoader() {
		super("src/main/resources", false);
	}

}

I can pull the mock object out of the application context using getBean(). From there, I can define the behavior on the mock object. This comes to an interesting point. There is at least some coupling going on at this point. The test is somewhat aware of the Service mock that will ultimately be invoked via the Controller. I imagine there may be a way to abstract this out, maybe to some parent test class that defines all the behaviors of all the service mock objects in the app. I’ve not yet gone to that level but its possible.

So now I have something a little bit stronger than my initial Controller unit tests. Now I can simply invoke mockMvc.perform(get(“/books”) and get back the full fledged xml response thats gone through jaxb marshalling and I can assert on the structure and content of the xml. In addition, I have assurance that all the Spring MVC infrastructure is in place and working. I can run it on the continuous build and get an assurance that no one has broken any Spring MVC annotations, JAXB annotations and any Spring MVC configurations in the app context.

The other extremely important thing to point out is that I in fact had to get rid of @Autowired on the controller and use @Resource.

Take a look at the tip in section 3.11.3 of http://static.springsource.org/spring/docs/2.5.x/reference/beans.html#beans-autowired-annotation.

The reason being is the Mockito mocked object is a proxied class of your Service interface. @Autowired resolves by type. @Resource resolves by bean name reference. This was just what was needed to resolve the (mock) Service dependency.

Take a look a the following links for reference. The framework is in github and not currently part of the Spring distribution.

https://github.com/SpringSource/spring-test-mvc/

https://github.com/SpringSource/spring-test-mvc/tree/master/src/test/java/org/springframework/test/web/server/samples/context

http://rstoyanchev.github.com/spring-31-and-mvc-test/#1

I also posted the complete project at http://dl.dropbox.com/u/60625371/spring-mvc-sandbox.zip. The maven pom file has the dependency for spring-mvc-test along with the repository defined.

I look forward to having the Spring MVC testing support included in future versions of Spring and a special thanks to Sam and Rossen over at Springsource!

10 Comments »

  1. Nice write-up. There are actually styles of tests you can write with spring-test-mvc.

    One is to load your Spring configuration — including the Spring MVC configuration, controllers, services, etc., with the Spring TestContext framework. This will test your controllers and you configuration at the same time and is close to the actual code used in production. On the other hand you may decide that you want to test your web layer separately from your services in which case you’ll need to mock or stub them. Hence the subject of your post above.

    The second style is to instantiate your controller directly and set it up with the mock or stub as you probably do in your current tests. Then you can register that controller directly with spring-test-mvc by using the “standaloneSetup” setup option in MockMvcBuilders. The effect is similar to using the mvc namespace, i.e. you get full support for annotated controllers and you can register custom message converters, interceptors, etc through the standalone setup builder. See for example the following test:

    https://github.com/SpringSource/spring-test-mvc/blob/master/src/test/java/org/springframework/test/web/server/samples/standalone/ViewResolutionTests.java

    I wonder if you’ve given this a try? And if so or if you do, do let us know what you think on the project home page on github!

    Comment by Rossen Stoyanchev — February 8, 2012 @ 4:08 pm

    • Rossen, Thanks. The issue with the standaloneSetup is we’d be bypassing our actual Spring MVC configuration for the app. The standAloneSetup seems like a little more heavy lifting than I think we were after. With the approach I outlined, we can leverage our actual Spring MVC configurations and by narrowing the context:component-scan and leveraging @Resource for injection in our Controllers we can achieve testing the configuration plus the Controller behavior with a mocked Service. The added overhead is to simply have a separate test context file which declares the mock services. Its just a slight spin on whats provided out of the box. I think all the approaches are valid depending on the app. I just wanted to share our approach and how we got away with short circuiting the @Autowired dependencies and let Spring resolve the mock objects. Thanks !

      Comment by stevelibonati — February 8, 2012 @ 5:10 pm

      • Thanks for the comment and for sharing your approach. I think it’s pretty good!

        Comment by Rossen Stoyanchev — February 10, 2012 @ 7:44 pm

  2. Hi
    A am a newbe on this and struggling to learn. I’ve downloaded your example and tried to understand.
    First.. there are two definitions like this:

    One in applicationContext.xml and the other in mockitoTestContext.xml. Is thir really corrrect?

    The other problem I have is… I translated your solution into my own,, but only with one “Mockito” bean in the mockitoTestContext.

    When I run my Junit test i get a error because Spring found two definitions for “bookService”. The one found in mockitoTestContext and the orginal implementation. This is understandable so there must be a way to force Spring to choose the “Mockito” version,,,, because this is the idea behind this setup. Keep the orginal configuration but still be able to replace some parts..

    As I sad,, I am a newbee,, but some one maby can help me?

    //lg

    Comment by Lars-Göran Lindström — August 8, 2012 @ 2:13 pm

    • Well, let me take a stab. It’s been a while since I played around with this. I brought it back up in Eclipse and I was running into a java.lang.ClassNotFoundException: org.springframework.core.annotation.AnnotationAttributes so I updated the version on the Spring dependencies to 3.1.1.RELEASE. I ran a maven install and was back in business.

      Now, to address your concerns … your asking if the two different applicationContext.xml and mockitoTestContext.xml files are necessary. The answer is yes. Its intended. Note that the mockitoTestContext.xml file is in src/test/resources and those bean definitions are specifically used in the unit test:

      @ContextConfiguration(loader = TestGenericWebXmlContextLoader.class, locations = {
      “classpath:applicationContext.xml”, “classpath:mockitoTestContext.xml” })

      However, I think the bean definition for bookService in applicationContext.xml is redundant and can be safely deleted. Sloppy refactoring on my part. I can see how that might have been confusing.

      So, BookServiceImpl is annotated with @Service which essentially makes it visible to Spring as a bean. The whole premise behind this is that we want to override that bean definition during the execution of the unit test and define the behavior on the mocked Service. The key to this all is really the annotaion on the reference for BookService in the BookController, which is @Resource. Please see the original post for the differences between @Resource and @Autowired. @Resource allows the bean to be referenced by name instead of type.

      In the call to: bookService = wac.getBean(BookService.class); the mocked bean is returned (because of the @Resource annotation).

      If I recall, Spring uses a different name for the bean, perhaps the entire fully qualified class name when using the @Autowired annotation, thats why using @Resource forces Spring to pick up the mocked bean in the mockitoTestContext.xml by name (specifically bookService in this case).

      Hope that helps. Let me know if you get it working. The only modifications I made to it are removing the redundant bean def in applicationContext.xml and updating the version in the pom.xml for the Spring dependencies and the test still runs successfully.

      Incidentally, I’ve incorporated the approach in this post to an ‘enterprise’ REST webapp and we’ve had good success with the ability to assert on the xml responses and ensure the Spring MVC configs are correct as well as being able to short circuit all the downstream dependencies that the Service objects may have (database daos, webservice daos, etc …) during the execution of the unit tests which are running on continuous integration (Jenkins).

      Comment by stevelibonati — August 8, 2012 @ 4:01 pm

      • As an afterthought …

        I think another key piece of this is by using @Service in the Service class definition and the fact that the class is named *Impl, is significant, com.test.service.book.impl.BookServiceImpl.

        The call to bookService = wac.getBean(BookService.class);

        is what would differentiate which bean gets pulled in , the mock or the Service class. I’m guessing Spring looks for an id of com.test.service.book.BookService, doesn’t find it and then tries again with bookService which of course is our mocked bean,

        as opposed to com.test.service.book.impl.BookServiceImpl which is defined by the @Service definition.

        Comment by stevelibonati — August 8, 2012 @ 4:17 pm

  3. Ok.. there is some “conventions” around naming that makes it work then… !!?? ( Is this kind of conventions documented anywhere??)

    My solution was to us Spring profiles.

    In my unit test I added @ActiveProfiles(“unitTest”).

    In mockitoTestContext.xml I added @profile=”unitTest” ti the beans declaration.
    To the BookServiceImpl I added @Profile(“production”)

    in web.xml I set the default profile to production

    Comment by Lars-Göran Lindström — August 12, 2012 @ 10:46 am

  4. Great post !!!! It saved my day (and night!).

    By the way, create Mockito instance in Spring XML, your way does not work because Mockito expects a Class instance, not a String.

    To circumvent it, rely on Spring EL (it’s magical)

    Notice the #{new com.test.service.book.BookService().getClass()}. Here we are creating an instance of the service just to get the class. It’s a little bit overkill and probably not performant but it’s the straightest way to have a Class instance

    Comment by doanduyhai — September 11, 2012 @ 9:47 pm

    • Sorry, wordpress striped out my XML

      <bean id=”bookService” class=”org.mockito.Mockito” factory-method=”mock”>
      <constructor-arg value=”#{new com.test.service.book.BookService().getClass()}” />
      </bean>

      Comment by doanduyhai — September 11, 2012 @ 9:49 pm

  5. Hello there! This article could not be written much better!
    Reading through this post reminds me of my previous roommate!
    He constantly kept preaching about this. I most certainly will forward this information to him.
    Pretty sure he’s going to have a good read. Thanks for sharing!

    Comment by ??? — April 20, 2013 @ 9:06 am


RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

The Shocking Blue Green Theme. Blog at WordPress.com.

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: