Unit testing Spring MVC 3 Web Components

The web layer is probably the most untested part of applications today. Why? For starters, testing web frameworks mystifies a lot of people. There are many parameters in web framework handler methods that seem to be beyond our control. There are also too many possibilities as the user interface seems to present infinite options for user input and can lead to many different outcomes. Not only that, but many people must figure that when it comes to testing the web layer, the only way to do that is by having a real person actually use the application… click click click… everything looks ok…

Why test web components? What should I test?

Is there really a point to testing web components? It looks like a lot of work. But, the answer is yes, in most cases. It should not completely replace end user testing, but it can catch bugs as the application evolves. Test driving the design of the web layer also forces developers and business analysts to think about the requirements for user input and validation. It forces us to ask ourselves the following:

What are the possible inputs?
What are the possible outcomes?
What can go wrong?
What must go right?
How must the application behave in each scenario?

We must ask ourselves these questions when testing all parts of the application, not just the web layer.

Now let’s revisit our GreetingController code and have a look at the UI as well, and remember what the application should do. Actually, for true Test Driven Development, we should have done this in the beginning, before developing any code. But I have decided to introduce unit testing at this later point in time because taking you through the TDD process from the beginning would have made these tutorials too long and overwhelming. Once you see the big picture, you will be able to start over with your next application and write the tests first (like you are supposed to!).

Identify your requirements

This application is very simple so testing may seem trivial. Sometimes it is hard to know where to begin when writing unit tests, so the best way to start is by writing a list of requirements. Let’s look at our GreetingController class and ask ourselves what exactly it is supposed to do.

What should this class do?

1. Get a greeting text from the user input
2. Get a ‘favorite color’ from the user input
3. If no color was selected, leave the default color as white
4. Find out what user is logged on and add that info to the Greeting object
5. Add the current date to the Greeting object
6. Add a Greeting
7. Display a list of greetings after the user added a greeting
8. Allow the user to go directly to the greetings list without adding a greeting
9. Validation? Oops, we forgot all about that! We will implement validation later and show how to ‘test drive’ this new functionality…

Unit testing Spring MVC 3 Web Components

Here is the cherry on the cake. The beauty of using Spring MVC 3 is that it is so easy to test. We can finally see why it makes sense to use a framework that does not require us to write handler methods with a particular signature, or use web forms or action classes or that extend a proprietary class. Now we are using Junit and Mockito mostly here, so we don’t even need to use Spring do our testing. (you don’t need Spring to test Spring ;) ) However Spring does come with some handy features that make testing easier so we will be using a little of that towards the end.

First let’s take a look at our GreetingController class. Let’s think about how we will write tests that will invoke the method addGreetingAndShowAll().

@RequestMapping(value = "/greetings.html", method = RequestMethod.POST)
public String addGreetingAndShowAll(@ModelAttribute("greetingform") GreetingForm greetingForm, 
        Map<String, Object> model) {		
 
	//get the greeting from the form (that contains what the user input to the form) 
	Greeting greeting = greetingForm.getGreeting();
 
	//set the date to the current date
	greeting.setGreetingDate(new Date());
 
	//find out what user is currently logged in and set the username to the greeting
	UserDetails userDetails = (UserDetails)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
	greeting.setUsername(userDetails.getUsername());
 
	//persist the greeting
	greetingService.addGreeting(greeting);  
 
	//prepare the greetings list to be displayed
	List<Greeting> greetings = greetingService.getAllGreetings();
	model.put("greetinglist", greetings);
 
    	String selectedColorCode=greetingForm.getColor().getColorCode(); 
    	if(selectedColorCode.equals("")) //if no color selected, then assign default
    		selectedColorCode="FFFFFF";
    	model.put("colorcode", selectedColorCode);
 
    	// This will resolve to /WEB-INF/jsp/greetings.jsp
    	return "greetings";
}

We would like to create some input to the method addGreetingAndShowAll() and we see there are 2 parameters, greetingForm and model. We must first initialize the GreetingForm object, with the Greeting and Color objects inside. We also have to create a new model object where the properties will eventually be set, and we can track that later.

public void testTheFirstTest() {
        //GIVEN
	GreetingForm greetingForm = new GreetingForm(); //first parameter of addGreetingAndShowAll()
	Greeting greeting = new Greeting();
	greetingForm.setGreeting(greeting);
	Color color = new Color();
	greetingForm.setColor(color);
	Map<String, Object> model = new HashMap<String, Object>(); //second parameter of addGreetingAndShowAll()
	//mock the GreetingService
	GreetingService fakeGreetingService = mock(GreetingService.class);
	List expectedGreetingList = new ArrayList<Greeting>();
	expectedGreetingList.add(greeting);
	GreetingController greetingController = new GreetingController(fakeGreetingService);
        //WHEN
	when(fakeGreetingService.getAllGreetings()).thenReturn(expectedGreetingList);
        greetingController.addGreetingAndShowAll(greetingForm, model);
        //THEN
        //assert something here....
}

Why the “fakeGreetingService”? We just want to test the GreetingController methods here, so we don’t want our tests to be affected by something that might go wrong in the GreetingService class. Keeping our tests focused on one class and isolated from dependencies means that we know exactly what we are testing. In other cases, GreetingService might not yet be implemented, or it may access parts of the application that are not available during the time of testing. So, with the help of Mockito, we can mock concrete classes or interfaces. Then we can program their methods to return an expected result. (with Mockito’s “when” and “andReturn”)

Writing testable code

One big advantage of writing tests at the same time that you develop your application code (or better yet, writing your tests first) is that your code becomes easier to test. If you put off writing unit tests until your application code becomes very complex, then you might find it not only becomes untestable, but also difficult to refactor into testable code.

Now let’s examine our GreetingController class to see if it is really ready for testing or if we have to make some changes. We are on our way to writing a unit test to have our controller call the method addGreetingAndShowAll(). So far so good, and now we can start asserting things soon! But wait, what happens when we get to this part?

UserDetails userDetails = (UserDetails)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
greeting.setUsername(userDetails.getUsername());

We get a NullPointerException because when we are running unit tests, the Authentication is null! That is obviously because there is no user logged in when the unit tests are running. How can we fix this problem? If we need access to something that is not available, in order to test something else, then the best thing to do is to mock the thing that is not available. Spring does not exactly make the testing of UserDetails easy for us. So, there are a few ways around this. One way is to create a class called UserService, which will handle getting the UserDetails. Then it will be easy for us to create a mock implementation of UserService and return a fake username for testing.

UserService.java
package com.bitbybit.service;
 
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
 
@Service
public class UserService {
 
	public UserDetails getUserDetails() {
	        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
	        if (authentication == null) {
                         return null;
                } else {
                         return (UserDetails) authentication.getPrincipal();
                }
	} 
 
	public String getUsername() {
		if (getUserDetails() == null) {
			return null;
		} else {
			return getUserDetails().getUsername();
		}	
	}
 
}


We can inject the UserService correctly into our GreetingController like this…

private UserService userService; 
 
@Autowired 
public GreetingController(GreetingService greetingService, UserService userService) { 
	this.greetingService = greetingService;
	this.userService = userService;
}

Now we can just modify this part of the code in addGreetingAndShowAll(), replacing this:

greeting.setUsername(userDetails.getUsername());

with this:

greeting.setUsername(userService.getUsername());

While we are at it, let’s replace this code:

if(selectedColorCode.equals("")) //if no color selected, then assign default
    	selectedColorCode="FFFFFF";

with this code:

//if no color selected, then assign default
if(selectedColorCode == null || selectedColorCode.equals("")) {
    	selectedColorCode=DEFAULT_FAVORITE_COLOR_CODE;
}

The new and improved GreetingController class

There, that’s better. Now GreetingController.java is ready for testing.

package com.bitbybit.web.controller;
 
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.apache.log4j.Logger;
import com.bitbybit.domain.Greeting;
import com.bitbybit.domain.Color;
import com.bitbybit.service.GreetingService;
import com.bitbybit.service.UserService;
import com.bitbybit.web.form.GreetingForm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
 
@Controller
@RequestMapping("/home") 
public class GreetingController {
 
	protected static Logger logger = Logger.getLogger("GreetingController");
 
	private final String DEFAULT_FAVORITE_COLOR_CODE = "FFFFFF";
 
	private GreetingService greetingService;
 
	private UserService userService; 
 
	//add this because it complains otherwise when doing security annotations
	//public GreetingController() {}
 
	@Autowired 
	public GreetingController(GreetingService greetingService, UserService userService) { //another change!!
		this.greetingService = greetingService;
		this.userService = userService;
	}
 
    //note there is no actual greetings.html file!! 
	@RequestMapping(value = "/addgreeting.html", method = RequestMethod.GET)
    public String showAddGreetingPage(@ModelAttribute("greetingform") GreetingForm greetingForm) {		
 
    	//resolves to /WEB-INF/jsp/addgreeting.jsp
    	return "addgreeting";
	}	
 
	@ModelAttribute("colorlist")
	public List<Color> populateColorList() {
		//color list is hardcoded for now...
		List<Color> colorList = new ArrayList<Color>();
		colorList.add(new Color("Indian Red", "F75D59"));
		colorList.add(new Color("Red", "FF0000"));
		colorList.add(new Color("Salmon", "F9966B"));
		colorList.add(new Color("Lemon Chiffon", "FFF8C6"));
		colorList.add(new Color("Olive Green", "BCE954"));
		colorList.add(new Color("Steel Blue", "C6DEFF"));
		colorList.add(new Color("Medium Purple", "9E7BFF"));
		return colorList;
	}	
 
	@RequestMapping(value = "/greetings.html", method = RequestMethod.POST)
	public String addGreetingAndShowAll(@ModelAttribute("greetingform") GreetingForm greetingForm,
			Map<String, Object> model) {		
 
		//get the greeting from the form (that contains what the user input to the form) 
		Greeting greeting = greetingForm.getGreeting();
 
		//set the date to the current date
		greeting.setGreetingDate(new Date());
 
		greeting.setUsername(userService.getUsername()); 
 
		//persist the greeting
		greetingService.addGreeting(greeting);  
 
		//prepare the greetings list to be displayed
		List<Greeting> greetings = greetingService.getAllGreetings();
		model.put("greetinglist", greetings);
 
    	String selectedColorCode=greetingForm.getColor().getColorCode(); 
    	//if no color selected, then assign default
    	if(selectedColorCode == null || selectedColorCode.equals("")) {
    		selectedColorCode=DEFAULT_FAVORITE_COLOR_CODE;
    	}
 
    	model.put("colorcode", selectedColorCode);
 
    	// This will resolve to /WEB-INF/jsp/greetings.jsp
    	return "greetings";
	} 
 
	//define the same url with GET so users can skip to the greetings page
	@RequestMapping(value = "/greetings.html", method = RequestMethod.GET)
	public String showAllGreetings(Map<String, Object> model) {		
 
		List<Greeting> greetings = greetingService.getAllGreetings();
		model.put("greetinglist", greetings);		
		model.put("colorcode", DEFAULT_FAVORITE_COLOR_CODE);
 
    	// This will resolve to /WEB-INF/jsp/greetings.jsp
    	return "greetings";
	}	
 
}

Unit tests for the GreetingController class

So here are the unit tests for testing almost all of our requirements. Is this the right way to test your application? Are these tests really worthwhile? Maybe, maybe not. What I want to achieve more than anything here is to show you how it is possible to test Spring MVC 3 web components, and next time leave it up to you to decide what to test. I want to show you how to write tests to invoke methods in your controller, and how to prepare the input. And, show you how to move most of the test preparation logic into the setUp() method to make your test code cleaner and to the point. I also want to show you that writing tests with names like testModelShouldContainGreetingWithUsername() are way better than methods with names like testShowAllGreetings() and testAddGreetingAndShowAll(). If a test does not communicate what the application should do, then the test has no meaning. So, my rule of thumb is: A unit test should contain the word “should.”

Take a look at the last two unit tests. These are different than the rest, because they test Spring’s handler. These tests enable us to verify that invoking the web container with certain RequestMapping parameters, for example “/greetings.html” and “GET”, results in a certain method being invoked. In this case, the method showAllGreetings() would be called. In these tests we have made use of some handy features in Spring’s test library. Spring’s objects MockHttpServletRequest and MockHttpServletResponse enable us to simulate the web container’s handling of our requests.

And finally, we see here how to use the Mockito framework. We see how to create a mock object of a concrete class or interface. We are mocking the concrete classes GreetingService and UserService here. From the perspective of our application requirements, it isn’t necessary to create interfaces for GreetingService or UserService just yet. So those continue to remain as concrete classes. But that hasn’t stopped us from testing and mocking! Later, in the “integration testing” section we will mock interfaces, but there is not much difference. In addition, we can understand here how Mockito works, particularly how to expect a method call and return a fake object with Mockito’s “when” and “andReturn” methods.

package com.bitbybit.web.controller;
 
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter;
import com.bitbybit.domain.Color;
import com.bitbybit.domain.Greeting;
import com.bitbybit.service.GreetingService;
import com.bitbybit.service.UserService;
import com.bitbybit.web.form.GreetingForm;
import static org.mockito.Mockito.*;
import junit.framework.TestCase;
 
public class GreetingControllerTest extends TestCase {
 
	private GreetingForm greetingForm; 
	Greeting greeting;
	Color color;
	Map<String, Object> model;
	GreetingService fakeGreetingService;
	List<Greeting> fakeGreetingList;
	UserService fakeUserService;
	GreetingController greetingController;
 
	@Before 
	protected void setUp() {
		//first initialize the 2 parameters of the method addGreetingAndShowAll() for testing
		greetingForm = new GreetingForm(); //first parameter of addGreetingAndShowAll()
		greeting = new Greeting();
		greetingForm.setGreeting(greeting);
		color = new Color();
		greetingForm.setColor(color);
		model = new HashMap<String, Object>(); //second parameter of addGreetingAndShowAll()
		//mock the GreetingService
		fakeGreetingService = mock(GreetingService.class);
		fakeGreetingList = new ArrayList<Greeting>();
		fakeGreetingList.add(greeting);
		//mock the UserDetails
		UserService fakeUserService = mock(UserService.class);
		when(fakeUserService.getUsername()).thenReturn("altheaparker");
		//inject the GreetingController with a fake GreetingService and UserService
		greetingController = new GreetingController(fakeGreetingService, fakeUserService);
		when(fakeGreetingService.getAllGreetings()).thenReturn(fakeGreetingList);
	}
 
	//test that the greeting text should be inserted into a Greeting object 
	//which ends up inside a list inside the model
	@Test
	public void testModelShouldContainNewGreetingText() {		
		//GIVEN
		greeting.setGreetingText("hello world");
		//WHEN
		greetingController.addGreetingAndShowAll(greetingForm, model);
		//THEN
		List<Greeting> greetingListResult = (ArrayList<Greeting>)(model.get("greetinglist")); 
		assertEquals("hello world", greetingListResult.get(0).getGreetingText());
	}
 
	//test that when the color red is selected, it is assigned correctly in the model
	@Test
	public void testModelShouldContainColorRedWhenSelected() {
		//GIVEN
		color.setColorCode("FF0000");
		//WHEN
		greetingController.addGreetingAndShowAll(greetingForm, model);
		//THEN
		assertEquals("FF0000", model.get("colorcode"));
	}	
 
	//test that when no color is selected, the default color should be white
	//the color should end up inside the model and is called 'colorcode'
	@Test
	public void testModelShouldContainColorWhiteWhenNoColorIsSelected() {		
		//GIVEN
		//no color value is initialized		
		//WHEN
		greetingController.addGreetingAndShowAll(greetingForm, model);
		//THEN
		assertEquals("FFFFFF", model.get("colorcode"));
	}	
 
	//test that the username makes it into the Greeting object inside the model
	@Test
	public void testModelShouldContainGreetingWithUsername() {
		//WHEN
		greetingController.addGreetingAndShowAll(greetingForm, model);
		//THEN
		List<Greeting> greetingListResult = (ArrayList<Greeting>)(model.get("greetinglist")); 
		assertEquals("altheaparker",  greetingListResult.get(0).getUsername());
	}	
 
	//test that the current date goes into the Greeting object inside the model
	@Test
	public void testModelShouldContainGreetingWithCurrentDate() {
		//GIVEN
		Date dateBeforeMethodCall = new Date();
		//WHEN
		greetingController.addGreetingAndShowAll(greetingForm, model);
		//THEN
		List<Greeting> greetingListResult = (ArrayList<Greeting>)(model.get("greetinglist")); 
		Date resultDate = greetingListResult.get(0).getGreetingDate(); 
		assertEquals(dateBeforeMethodCall.getTime(), resultDate.getTime());		
	}	
 
	//test that when a new Greeting is created, it ends up inside a list inside the model
	@Test
	public void testNewGreetingShouldBeInsertedIntoList() {
		//WHEN
		greetingController.addGreetingAndShowAll(greetingForm, model);		
		//THEN
		List<Greeting> greetingListResult = (ArrayList<Greeting>)(model.get("greetinglist")); 
		assertNotNull(greetingListResult);
		assertEquals(greetingListResult.size(), 1);		
	}
 
	//when the user skips directly to the greetings page without entering a greeting....
	//given @RequestMapping(value = "/greetings.html", method = RequestMethod.GET)
	//showAllGreetings() method should be called
	//and "greetings" should be returned and default color should be white 
	@Test 
	public void testShowAllGreetingsMethodShouldBeCalledWithGET() throws Exception {
		//GIVEN
		AnnotationMethodHandlerAdapter handlerAdapter = new AnnotationMethodHandlerAdapter();
		MockHttpServletRequest request = new MockHttpServletRequest("GET","/home/greetings.html");
                MockHttpServletResponse response = new MockHttpServletResponse();
                //WHEN
                ModelAndView mav = handlerAdapter.handle(request, response, greetingController);
                //THEN
                assertEquals("greetings", mav.getViewName());
                assertEquals("FFFFFF", mav.getModel().get("colorcode"));
	}
 
	//when the user adds a greeting....
	//given @RequestMapping(value = "/greetings.html", method = RequestMethod.POST)
	//addGreetingAndShowAll() method should be called
	//and "greetings" should be returned	
	@Test 
	public void testAddGreetingAndShowAllMethodShouldBeCalledWithPOST() throws Exception {
		//GIVEN
		AnnotationMethodHandlerAdapter handlerAdapter = new AnnotationMethodHandlerAdapter();
		MockHttpServletRequest request = new MockHttpServletRequest("POST","/home/greetings.html");
                MockHttpServletResponse response = new MockHttpServletResponse();
                //WHEN
                ModelAndView mav = handlerAdapter.handle(request, response, greetingController);
                //THEN
                assertEquals("greetings", mav.getViewName());
	}	
 
}

Now that we understand what testing is all about, we will see how to test drive the design for input validation in a later chapter. So stay tuned :)



2 Comments
aprajitha wrote on 2012-08-21 at 15:15:05:
Are you sure, the last two tests ( testAddGreetingAndShowAllMethodShouldBeCalledWithGET and testAddGreetingAndShowAllMethodShouldBeCalledWithPOST) will work without loading an applicationContext (say, using SpringJUnit4Runner.class) I am getting a NPE, as listed below. java.lang.NullPointerException at org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter$ServletHandlerMethodResolver.useTypeLevelMapping(AnnotationMethodHandlerAdapter.java:675) at org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter$ServletHandlerMethodResolver.resolveHandlerMethod(AnnotationMethodHandlerAdapter.java:585) at org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter.invokeHandlerMethod(AnnotationMethodHandlerAdapter.java:431) at org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter.handle(AnnotationMethodHandlerAdapter.java:424) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at java.lang.reflect.Method.invoke(Method.java:597) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:45) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:42) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20) at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:28) at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:263) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:68) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:47) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:231) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:50) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222) at org.junit.runners.ParentRunner.run(ParentRunner.java:300) at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50) at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)


aprajitha wrote on 2012-08-21 at 04:10:07:
Thanks for excellent post. Do you have the one for integration testing. http://bitbybitblog.com/?p=678 does not seem to work.


Post a Comment

(required):