Category Archives: Unit Testing

Java: Promoting Testability by Having Enum Implementing an Interface

OVERVIEW

This post illustrates how we can easily write a better test case without polluting our production code with non-production code by performing a minor refactoring to the production code.

PROBLEM

Let’s assume we have a simple Data Reader that reads all the lines of a given algorithm data file and returns them:-

public class DataReader {
    public List<String> getDataLines(AlgorithmEnum algorithm) {
        // we have `StS-data.txt`, `CtE-data.txt` and `TtI-data.txt` under `src/main/resources` dir
        String fileName = String.format("%s-data.txt", algorithm.getShortName());
        Scanner scanner = new Scanner(getClass().getClassLoader().getResourceAsStream(fileName));

        List<String> list = new ArrayList<String>();

        while (scanner.hasNextLine()) {
            list.add(scanner.nextLine());
        }

        return list;
    }
}

This API accepts an AlgorithmEnum and it looks something like this:-

public enum AlgorithmEnum {
    SKIN_TO_SKIN("StS"),
    CLOSURE_TO_EXIT("CtE"),
    TIME_TO_INCISION("TtI");

    private String shortName;
    
    AlgorithmEnum(String shortName) {
        this.shortName = shortName;
    }

    public String getShortName() {
        return shortName;
    }
}

Let’s assume each algorithm data file has millions of data lines.

So, how do we test this code?

SOLUTION 1: Asserting Actual Line Count == Expected Line Count

One straightforward way is to:-

  • Pass in one of the Enum constants (AlgorithmEnum.SKIN_TO_SKIN, etc) into DataReader.getDataLines(..)
  • Get the actual line counts
  • Assert the actual line counts against the expected line counts

public class DataReaderTest {
    @Test
    public void testGetDataLines() {
        List<String> lines = new DataReader().getDataLines(AlgorithmEnum.SKIN_TO_SKIN);
        assertThat(lines, hasSize(7500));
    }
}    

This is a pretty weak test because we only check the line counts. Since we are dealing with a lot of data lines, it becomes impossible to verify the correctness of each data line.

SOLUTION 2: Adding a Test Constant to AlgorithmEnum

Another approach is to add a test constant to AlgorithmEnum:-

public enum AlgorithmEnum {
    SKIN_TO_SKIN("StS"),
    CLOSURE_TO_EXIT("CtE"),
    TIME_TO_INCISION("TtI"),
		
    // added a constant for testing purpose
    TEST_ABC("ABC");

    private String shortName;
    
    AlgorithmEnum(String shortName) {
        this.shortName = shortName;
    }

    public String getShortName() {
        return shortName;
    }
}

Now, we can easily test the code with our test data stored at src/test/resources/ABC-data.txt:-

public class DataReaderTest {
    @Test
    public void testGetDataLines() {
        List<String> lines = new DataReader().getDataLines(AlgorithmEnum.TEST_ABC);
        assertThat(lines, is(Arrays.asList("line 1", "line 2", "line 3")));
    }
}

While this approach works, we pretty much polluted our production code with non-production code, which may become a maintenance nightmare as the project grows larger in the future.

SOLUTION 3: AlgorithmEnum Implements an Interface

Instead of writing a mediocre test case or polluting the production code with non-production code, we can perform a minor refactoring to our existing production code.

First, we create a simple interface:-

public interface Algorithm {
    String getShortName();
}

Then, we have AlgorithmEnum to implement Algorithm:-

public enum AlgorithmEnum implements Algorithm {
    SKIN_TO_SKIN("StS"),
    CLOSURE_TO_EXIT("CtE"),
    TIME_TO_INCISION("TtI");

    private String shortName;

    AlgorithmEnum(String shortName) {
        this.shortName = shortName;
    }

    public String getShortName() {
        return shortName;
    }
}

Now, instead of passing AlgorithmEnum into getDataLines(...), we will pass in Algorithm interface.

public class DataReader {
    public List<String> getDataLines(Algorithm algorithm) {
        String fileName = String.format("%s-data.txt", algorithm.getShortName());
        Scanner scanner = new Scanner(getClass().getClassLoader().getResourceAsStream(fileName));
    
        List<String> list = new ArrayList<String>();
    
        while (scanner.hasNextLine()) {
            list.add(scanner.nextLine());
        }

        return list;
    }
}

With these minor changes, we can easily unit test the code with our mock data stored under src/test/resources directory.

public class DataReaderTest {
    @Test
    public void testGetDataLines() {
        List<String> lines = new DataReader().getDataLines(new Algorithm() {
            @Override
            public String getShortName() {
                // we have `ABC-data.txt` under `src/test/resources` dir
                return "ABC";
            }
        });

        assertThat(lines, is(Arrays.asList("line 1", "line 2", "line 3")));
    }
}

MockMvc : Circular view path [view]: would dispatch back to the current handler URL [/view] again

PROBLEM

Let’s assume we want to test this controller:-

@Controller
@RequestMapping(value = "/help")
public class HelpController {

    @RequestMapping(method = RequestMethod.GET)
    public String main() {
        return "help";
    }
}

Here’s the test file:-

public class HelpControllerTest {

    private MockMvc mockMvc;

    @Before
    public void setup() {
        mockMvc = MockMvcBuilders.standaloneSetup(new HelpController()).build();
    }

    @Test
    public void main() throws Exception {
        mockMvc.perform(get("/help"))
                .andExpect(status().isOk())
                .andExpect(view().name("help"));
    }
}

When executing this test, we get the following error:-

javax.servlet.ServletException: Circular view path [help]: would dispatch 
back to the current handler URL [/help] again. Check your ViewResolver 
setup! (Hint: This may be the result of an unspecified view, due to default 
view name generation.)
	at org.springframework.web.servlet.view.InternalResourceView.prepareForRendering(InternalResourceView.java:263)
	at org.springframework.web.servlet.view.InternalResourceView.renderMergedOutputModel(InternalResourceView.java:186)
	at org.springframework.web.servlet.view.AbstractView.render(AbstractView.java:266)

SOLUTION

The reason this is happening is because the uri “/help” matches the returned view name “help” and we didn’t set a ViewResolver when contructing the standalone MockMvc. Since MockMvcBuilders.standaloneSetup(...) doesn’t load Spring configuration, the Spring MVC configuration under WEB-INF/spring-servlet.xml will not get loaded too.

A typical WEB-INF/spring-servlet.xml looks something like this:-

<?xml version="1.0" encoding="UTF-8"?>
<beans ...>

    <context:component-scan base-package="edu.mayo.requestportal.controller"/>

    <mvc:annotation-driven/>

    <mvc:resources location="/resources/" mapping="/resources/**"/>

    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/view/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename" value="messages"/>
    </bean>
</beans>

To fix this, we need to defined a ViewResolver that mimics the configuration defined under WEB-INF/spring-servlet.xml in the test file:-

public class HelpControllerTest {

    private MockMvc mockMvc;

    @Before
    public void setup() {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/jsp/view/");
        viewResolver.setSuffix(".jsp");

        mockMvc = MockMvcBuilders.standaloneSetup(new HelpController())
                                 .setViewResolvers(viewResolver)
                                 .build();
    }

    @Test
    public void main() throws Exception {
        mockMvc.perform(get("/help"))
                .andExpect(status().isOk())
                .andExpect(view().name("help"));
    }
}

MockMvc + Mockito = Epic Tests

Spring Framework 3.2 introduces a very elegant way to test Spring MVC controller using MockMvc.

Based on the documentation, there are two ways to configure MockMvc:-

  • MockMvcBuilders.webAppContextSetup(webApplicationContext).build()
  • MockMvcBuilders.standaloneSetup(controller).build()

The first approach will automatically load the Spring configuration and inject WebApplicationContext into the test. The second approach does not load the Spring configuration.

While both options work, my preference is to use the second approach that doesn’t load the Spring configuration. Rather, I use Mockito to mock out all the dependencies within the controller.

EXAMPLE

Let’s assume we want to test this controller:-

@Controller
@RequestMapping(value = "/comment/{uuid}")
public class CommentController {

    @Autowired
    private RequestService requestService;

    @Autowired
    private CommentValidator validator;

    @InitBinder("commentForm")
    protected void initBinder(WebDataBinder binder) {
        binder.setValidator(validator);
    }

    @RequestMapping(method = RequestMethod.POST)
    public String saveComment(@PathVariable String uuid,
                              @Valid @ModelAttribute CommentForm commentForm,
                              BindingResult result,
                              Model model) {

        RequestComment requestComment = requestService.getRequestCommentByUUID(uuid);

        if (requestComment == null) {
            return "redirect:/dashboard";
        }

        if (result.hasErrors()) {
            return "comment";
        }

        return "ok";
    }
}

SETTING UP TEST FILE

To test /comment/{uuid} POST, we need three tests:-

  • requestComment is null, which returns redirect:/dashboard view.
  • Form validation contains error, which returns comment view.
  • Everything works fine, which returns ok view.

The test file looks something like this:-

public class CommentControllerTest {

    @Mock
    private RequestService requestService;

    @Mock
    private CommentValidator validator;

    @InjectMocks
    private CommentController commentController;

    private MockMvc mockMvc;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);

        mockMvc = MockMvcBuilders.standaloneSetup(commentController).build();

        when(validator.supports(any(Class.class))).thenReturn(true);
    }

    @Test
    public void testSaveComment_RequestCommentNotFound() throws Exception {
		...
    }

    @Test
    public void testSaveComment_FormError() throws Exception {
		...
    }

    @Test
    public void testSaveComment_NoError() throws Exception {
		...
    }
}

Because the controller has two dependencies ( RequestService and CommentValidator ) injected into it through Spring autowiring, we are going to create these two mocks and inject them into the controller by annotating them with Mockito’s @Mock and @InjectMocks accordingly.

In setup(...) method, the first line initializes objects annotated with @Mock. The second line initializes MockMvc without loading Spring configuration because we want the flexibility to mock the dependencies out using Mockito. The third line instructs the validator to return true when validator.support(...) is invoked. If the third line is left out, you will get this exception when binder.setValidator(validator) in the controller.initBinder(...) is invoked:-

org.springframework.web.util.NestedServletException: Request processing failed; 
nested exception is java.lang.IllegalStateException: Invalid target for Validator 
[validator]: com.choonchernlim.epicapp.form.CommentForm@1a80b973

Finally, we have three stubs to test the conditions defined earlier.

TEST CASE 1: requestComment is null

public class CommentControllerTest {

	...

    @Test
    public void testSaveComment_RequestCommentNotFound() throws Exception {
        when(requestService.getRequestCommentByUUID("123")).thenReturn(null);

        mockMvc.perform(post("/comment/{uuid}", "123"))
                .andExpect(status().isMovedTemporarily())
                .andExpect(view().name("redirect:/dashboard"));
    }

}

The first test is rather easy. All we need to do is to instruct requestService.getRequestCommentByUUID(...) to return null when it is invoked.

TEST CASE 2: Form validation contains error

public class CommentControllerTest {

	...

    @Test
    public void testSaveComment_FormError() throws Exception {
        when(requestService.getRequestCommentByUUID("123")).thenReturn(new RequestComment());

        doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
                Errors errors = (Errors) invocationOnMock.getArguments()[1];
                errors.reject("forcing some error");
                return null;
            }
        }).when(validator).validate(anyObject(), any(Errors.class));

        mockMvc.perform(post("/comment/{uuid}", "123"))
                .andExpect(status().isOk())
                .andExpect(view().name("comment"));
    }
}

The second test is a little complicated. We need to instruct requestService.getRequestCommentByUUID(...) to return a RequestComment object. Then, we use Mockito’s doAnswer(...) to set a dummy error value to Errors object, which is the second argument of validator.validate(...). Setting this value will cause result.hasErrors() to evaluate to true.

TEST CASE 3: Everything works fine

public class CommentControllerTest {

	...

    @Test
    public void testSaveComment_NoError() throws Exception {
        when(requestService.getRequestCommentByUUID("123")).thenReturn(new RequestComment());

        mockMvc.perform(post("/comment/{uuid}", "123"))
                .andExpect(status().isOk())
                .andExpect(view().name("ok"));
    }
}

The third test is rather easy too. We need to instruct requestService.getRequestCommentByUUID(...) to return a RequestComment object… and that’s it.

CONCLUSION

See… it’s not really that hard. The combination of Mockito and Spring Test Framework 3.2 provides us a great flexibility to test our Spring MVC controllers. Granted, we should already have our unit tests for the two dependencies ( RequestService and CommentValidator ) already. So, it is safe to alter the behavior of these dependencies using Mockito to test every possible path in the controller.

How to Unit Test Spring MVC Controller

SCENARIO

Let’s assume we have the following controller that needs to be tested:-

@Controller
@RequestMapping(value = "/person")
public class PersonController {

    private PersonService personService;

    @Autowired
    public PersonController(PersonService personService) {
        this.personService = personService;
    }

    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    public String getPerson(@PathVariable Long id, Model model) {
        model.addAttribute("personData", personService.getPerson(id));
        return "personPage";
    }
}

SOLUTION 1: “Works but It Won’t Get You the Promotion”

This working solution relies on:-

  • JUnit – General unit test framework.
  • Mockito – To mock PersonService.

public class PersonControllerTest {

    @Test
    public void testGetPerson() {

        PersonService personService = mock(PersonService.class);

        when(personService.getPerson(1L)).thenReturn(new Person(1L, "Chuck"));

        PersonController controller = new PersonController(personService);

        Model model = new ExtendedModelMap();

        String view = controller.getPerson(1L, model);

        assertEquals("View name", "personPage", view);

        Person actualPerson = (Person) model.asMap().get("personData");

        assertEquals("matching ID", Long.valueOf(1), actualPerson.getId());
        assertEquals("matching Name", "Chuck", actualPerson.getName());
    }
}

While this solution works, but it has a few problems. This test case strictly tests the actual controller API, but it completely disregards the URI and request method (GET, POST, PUT, DELETE, etc). For instance:-

  • What if the URI has a typo ( /persn/1 ) or it is not properly constructed ( /person/donkey-kong )?
  • What if the request method should be a POST but we accidentally used a GET?

SOLUTION 2: “A Mind Blowing Solution that Still Won’t Get You the Promotion but You Feel So Invincible That You Feel Compelled to Flip a Table Over”

This better solution relies on:-

  • JUnit – General unit test framework.
  • Mockito – To mock PersonService.
  • Spring MVC Test Framework – To properly test the controller.
  • Hamcrest – To clean way to assert the actual result is correct.

@RunWith(SpringJUnit4ClassRunner.class)
// This XML configuration basically enable component scanning.
// You could have used @Configuration and @ComponentScan to do the same thing.
@ContextConfiguration({"classpath*:spring-test.xml"})
public class PersonControllerTest {

    @Mock
    private PersonService personService;

    @InjectMocks
    private PersonController personController;

    private MockMvc mockMvc;

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

    @Test
    public void testGetPerson() throws Exception {
        when(personService.getPerson(1L)).thenReturn(new Person(1L, "Chuck"));

        mockMvc.perform(get("/person/{id}", 1L))
                .andExpect(status().isOk())
                .andExpect(view().name("personPage"))
                .andExpect(model().attribute("personData",
                                             allOf(hasProperty("id", is(1L)),
                                                   hasProperty("name", is("Chuck")))));
    }
}

Basically object that is annotated with @Mock will get injected into object that is annotated with InjectMocks. Then, we rely on Spring MVC Test Framework’s MockMvc to test our controller in a very clean and detailed fashion.

Okay, you may flip a table over now…

By the way…

You need at least Spring 3.2 to use MockMvc.

<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-test</artifactId>
	<version>3.2.4.RELEASE</version>
	<scope>test</scope>
</dependency>

If you are using an older Mockito version, you will get this error:-

org.mockito.exceptions.base.MockitoException: Field 'personController' annotated with @InjectMocks is null.
Please make sure the instance is created *before* MockitoAnnotations.initMocks();
Example of correct usage:
   class SomeTest {
      @InjectMocks private Foo foo = new Foo();
      
      @Before public void setUp() {
         MockitoAnnotations.initMock(this);

Upgrading Mockito to the latest version will fix this error:-

<dependency>
	<groupId>org.mockito</groupId>
	<artifactId>mockito-core</artifactId>
	<version>1.9.5</version>
	<scope>test</scope>
</dependency>