JUnit for C# Developers 4 – BDD, Mocks, and Matchers
This is yet another in my series of posts on using JUnit from the perspective of a C# developer.
Goals
Today, I have the following goals in my quest for JUnit TDD proficiency.
- Use a BDD-style testing scheme with nested classes.
- Use mocking framework to verify method call
- Use mocking framework to verify method call with parameters.
Getting to Work
First up, I’d like to see how to employ the test organization scheme described in this post by Phil Haack. The idea is that rather than simply having a test class per class under test, you’ll have a test class and nest within it a sub class for each method in the class under test.
Under Drew’s system, I’ll have a corresponding top level class, with two embedded classes, one for each method. In each class, I’ll have a series of tests for that method.
When you look at this in the test-runner, you see the same descriptive name, but the tests are better organized and can be run at another level of granularity. I’ve come to favor this style when I’m writing code in C#, and I thought I’d see how well it ported to JUnit. As it turns out, the test runner ignores the tests if you simply stick them in sub-classes. I poked around a little and discovered a post by Joshua Lockwood where he had the same idea and found a solution. I tried this out and it got me almost all the way there. I did need one minor tweak, however. (His post was written in 2008, so plenty may have changed in the interim). The “Enclosed” class that he uses required me to import “org.junit.experimental.runners.Enclosed”. By adding this line, I was off and running (though I did have to manually add the import as the IDE didn’t seem to find it):
package com.daedtech.daedalustest.controller;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import org.junit.experimental.runners.Enclosed;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import com.daedtech.daedalus.controller.LightController;
import com.daedtech.daedalus.services.LightManipulationService;
@RunWith(Enclosed.class)
public class LightControllerTest {
private static LightController buildTarget() {
return buildTarget(null);
}
private static LightController buildTarget(LightManipulationService service) {
LightManipulationService myService = service != null ? service : mock(LightManipulationService.class);
return new LightController(myService);
}
public static class Constructor {
@Test(expected=IllegalArgumentException.class)
public void throws_Exception_On_Null_Service_Argument() {
new LightController((LightManipulationService)null);
}
}
public static class light {
@Test
public void returns_Instance_Of_ModelAndView() {
LightController myController = buildTarget();
Assert.isInstanceOf(ModelAndView.class, myController.light());
}
@Test
public void is_Decorated_With_RequestMapping_Annotation() throws NoSuchMethodException, SecurityException {
Class myClass = LightController.class;
Method myMethod = myClass.getMethod("light");
Annotation[] myAnnotations = myMethod.getDeclaredAnnotations();
Assert.isTrue(myAnnotations[0] instanceof RequestMapping);
}
@Test
public void requestMapping_Annotation_Has_Parameter_Light() throws NoSuchMethodException, SecurityException {
Class myClass = LightController.class;
Method myMethod = myClass.getMethod("light");
Annotation[] myAnnotations = myMethod.getDeclaredAnnotations();
String myAnnotationParameter = ((RequestMapping)myAnnotations[0]).value()[0];
assertEquals("/light", myAnnotationParameter);
}
}
}
Notice the class annotation and the new static nested classes. These nested classes do have to be public and static for the scheme to work. In addition, it seems that once you use the “Run With Enclosed” paradigm, all tests must be in enclosed static classes to run. If you had some defined in the test class itself, the test runner would ignore them.
So, now that organization is better, onto more concrete matters. I now want to use my mocking framework to verify that a method was called. I want to add a method to the controller that takes a room name, a light name, and a text command (“on” or “off”) and issues a command to the service based on that. Using Mockito, I wrote the following test:
@Test
public void calls_service_toggleLight_method() {
LightManipulationService myService = mock(LightManipulationService.class);
LightController myController = buildTarget(myService);
myController.toggleLight("asdf", "fdsa", "on");
verify(myService).toggleLight((Light)anyObject(), anyBoolean());
}
The statement at the end is the equivalent of the “assert” here. I start out by building a mock using Mockito, and then I hand it to my overloaded builder, which injects it into my CUT. I perform (or will perform, since this method isn’t yet defined) an operation on the controller, and then I want to verify that performing that method resulted in a call to the interface’s toggleLight() method. The “any” parameters are known as “matchers” and they can be used in tests not just to see if a method on a collaborator was called, but with what kinds of parameters.
In the C# world, I use Moq and am a big fan of it. If you use this in C#, this whole paradigm should look pretty familiar. We create a mock, inject it, manipulate it, and verify it. Verify here is a static method that takes the mock as an argument, rather than an instance method of the mock, and mock creation is the same, but beyond that, these constructs look very similar, right down to the static “any()” methods for argument matching.
My final goal was to get to the point of using the aforementioned matchers to make sure the service methods were being invoked as I envisioned. To make the last test pass, I wrote the following “simplest possible” TDD code:
public void toggleLight(String room, String light, String command) {
_lightService.toggleLight(null, false);
}
Since the unit test allowed for any Light object and any boolean to be the parameters, I opted for null and false, respectively. Doesn’t get much simpler than that. To advance my goals a bit, I know that when the command string passed to the method is “on”, I want to call the service with boolean parameter true. So, let’s see how that test would look:
@Test
public void calls_service_toggleLight_with_isOn_true_when_passed_command_on() {
LightManipulationService myService = mock(LightManipulationService.class);
LightController myController = buildTarget(myService);
myController.toggleLight("asdf", "fdsa", "on");
verify(myService).toggleLight((Light)anyObject(), eq(true));
}
It’s a nearly identical test, but this time around, notice that I’ve traded “anyBoolean()” for “eq(true)”. Now this test will only pass if the toggleLight() method calls the service with boolean true. the eq() static method returns a matcher for a specific value. Getting all tests to pass is pretty straightforward here:
public void toggleLight(String room, String light, String command) {
_lightService.toggleLight(null, true);
}
Obviously, this method is pretty obtuse and needs some work, but I’ll get to that in the “off” command parameter case. The beauty of TDD is that you go from obtuse to rigor and accuracy by adding only the complexity you need in order to satisfy the next requirement. So, to recap, here is the current state of affairs of the controller:
@Controller
@RequestMapping("/light")
public class LightController {
private LightManipulationService _lightService;
public LightController(LightManipulationService lightManipulationService) {
if(lightManipulationService == null) throw new IllegalArgumentException("lightManipulationService");
_lightService = lightManipulationService;
}
@RequestMapping("/light")
public ModelAndView light() {
return new ModelAndView();
}
/**
* Toggles the light described by room and light names on or off (command)
* @param room - Name of the room we find this light in
* @param light - Name of the light itself
* @param command - Whether to turn the light on or off
*/
public void toggleLight(String room, String light, String command) {
_lightService.toggleLight(null, true);
}
}
and the test class:
@RunWith(Enclosed.class)
public class LightControllerTest {
private static LightController buildTarget() {
return buildTarget(null);
}
private static LightController buildTarget(LightManipulationService service) {
LightManipulationService myService = service != null ? service : mock(LightManipulationService.class);
return new LightController(myService);
}
public static class Constructor {
@Test(expected=IllegalArgumentException.class)
public void throws_Exception_On_Null_Service_Argument() {
new LightController((LightManipulationService)null);
}
}
public static class light {
@Test
public void returns_Instance_Of_ModelAndView() {
LightController myController = buildTarget();
Assert.isInstanceOf(ModelAndView.class, myController.light());
}
@Test
public void is_Decorated_With_RequestMapping_Annotation() throws NoSuchMethodException, SecurityException {
Class myClass = LightController.class;
Method myMethod = myClass.getMethod("light");
Annotation[] myAnnotations = myMethod.getDeclaredAnnotations();
Assert.isTrue(myAnnotations[0] instanceof RequestMapping);
}
@Test
public void requestMapping_Annotation_Has_Parameter_Light() throws NoSuchMethodException, SecurityException {
Class myClass = LightController.class;
Method myMethod = myClass.getMethod("light");
Annotation[] myAnnotations = myMethod.getDeclaredAnnotations();
String myAnnotationParameter = ((RequestMapping)myAnnotations[0]).value()[0];
assertEquals("/light", myAnnotationParameter);
}
}
public static class toggleLight {
@Test
public void calls_service_toggleLight_method() {
LightManipulationService myService = mock(LightManipulationService.class);
LightController myController = buildTarget(myService);
myController.toggleLight("asdf", "fdsa", "on");
verify(myService).toggleLight((Light)anyObject(), anyBoolean());
}
@Test
public void calls_service_toggleLight_with_isOn_true_when_passed_command_on() {
LightManipulationService myService = mock(LightManipulationService.class);
LightController myController = buildTarget(myService);
myController.toggleLight("asdf", "fdsa", "on");
verify(myService).toggleLight((Light)anyObject(), eq(true));
}
}
}