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.
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