Tuesday, June 1, 2010

Auto injection in jUnit

After doing Java for many years, and TDD for several, I’ve settled into a fairly consistent way of designing and testing my classes. Any time you start to following a pattern in your code, if you’re paying attention, you’ll notice you’re writing the same code over and over. Now, it usually isn’t exactly the same or you’d easily just move it into a class and use the class. No, it’s usually code that is different every time, but it’s essentially doing the same thing, just with different classes. This is one example of what is generally referred to as boilerplate code. It’s aso one of the subtle forms of code duplication, and it’s something you want to avoid as much as possible.

I recently noticed that due to how I write my classes and tests, around 95% or more of my setup methods in my tests consisted of creating the class to be tested, creating some mocks, stubs, or fakes, and then injecting them into the class. Sure, technically every setup method was different, but this was boilerplate code for sure. And it was starting to get on my nerves. The setup usually looked something like this:

public class TestFoo {
   private Foo foo;
   private DependencyOne dependencyOne;
   private DependencyTwo dependencyTwo;

   @Before
   public void setup() {
      foo = new Foo();
      dependencyOne = new DependencyOneMock();
      dependencyTwo = new DependencyTwoMock();
      foo.setDependencyOne(dependencyOne);
      foo.setDependencyTwo(dependencyTwo);
   }
   ... then all the tests
}

One of the best ways to attack boilerplate code is by using the concept of convention over configuration. Basically all my setup methods were just configuration of the class I was testing, so what I needed to do is come up with a convention that made the configuration unnecessary, and some way to act upon the convention.

The convention starts with the @Target annotation. The idea is that you put this annotation on a field to signify that the field is the target of the test class that will need to have dependencies injected into it. You can also put @Target on a method, and whatever is returned from the method will be used as the target for injection.

Secondly, when you define a field on the test class if it matches a setter method on the target class, then it will be injected into the target using the setter. If no setter is found, but there is a field with the same name on the target, then the value will be copied to the field on the target.

In order to actually act on this convention I used the jUnit @Rule annotation. I called my rule AutoMockAndInject. If you aren’t familiar with how this annotation works, you can read about it here. I will put the code for my rule and the target annotation at the bottom of this post.

So, following the convention and using the rule my test class now looks like this:

public class TestFoo {
@Rule public AutoMockAndInject autoInject = new AutoMockAndInject();
@Target private Foo foo = new Foo();
private DependencyOne dependencyOne = new DependencyOneMock();
private DependencyTwo dependencyTwo = new DependencyTwoMock();

   ... then all the tests
}

One thing to remember here is that jUnit creates a new instance of the test class for each test method it runs. Otherwise, doing it this way could cause some problems.

I also use Mockito when I don’t want to make a hand written mock. The AutoMockAndInject rule works with the @Mock annotation from Mockito. It will create the mock object and inject it into the target. So if I want to use Mocito for my dependencies instead of hand written ones, the test class would look like this:

public class TestFoo {
@Rule public AutoMockAndInject autoInject = new AutoMockAndInject();
@Target private Foo foo = new Foo();
@Mock private DependencyOne dependencyOne;
@Mock private DependencyTwo dependencyTwo;

   ... then all the tests
}

I just started doing this a couple weeks ago, and so far I like it. It’s cut down on a lot of boilerplate code. But, in my experience, it usually takes at least a few months of doing something before you really see if it was a good idea or not. So, we’ll see if in the long run it really makes thing better.

Here is the code for my annotation and the jUnit rule I used for doing this.

@Retention(RetentionPolicy.RUNTIME)
public @interface Target {
}

public class AutoMockAndInject implements MethodRule {
   private static final String specialFields = "$VRc,serialVersionUID";
   private Object target;

   public final Statement apply(final Statement base, FrameworkMethod method, final Object target) {
      return new Statement() {
         @Override public void evaluate() throws Throwable {
            before(target);
            base.evaluate();
         }
      };
   }

   protected void before(Object source) throws Throwable {
      createMockitoMocks(source);
      if (hasTargetAnnotation(source))
      autoInject(source);
   }

   private void createMockitoMocks(Object source) {
      MockitoAnnotations.initMocks(source);
   }

   private boolean hasTargetAnnotation(Object source) throws Exception {
      return hasTargetFiled(source) || hasTargetMethod(source);
   }

   private boolean hasTargetMethod(Object source) throws Exception {
      for (Method method : source.getClass().getMethods()) {
         if (method.getAnnotation(Target.class) != null) {
            target = method.invoke(source);
            return true;
         }
      }
      return false;
   }

   private boolean hasTargetFiled(Object source) throws Exception {
      for (Field field : getAllFields(source.getClass())) {
         if (field.getAnnotation(Target.class) != null) {
            target = getFieldValue(source, field);
            return true;
         }
      }
      return false;
   }

   private Object getFieldValue(Object target, Field field) throws Exception {
      field.setAccessible(true);
      return field.get(target);
   }

   private Set<Field> getAllFields(Class<?> clazz) {
      return getAllFields(new HashSet<Field>(), clazz);
   }

   private Set<Field> getAllFields(Set<Field> fields, Class<?> clazz) {
      for (Field field : clazz.getDeclaredFields())
         if (notSpecialField(field))
            fields.add(field);
      if (clazz.getSuperclass() != null)
         getAllFields(fields, clazz.getSuperclass());
      return fields;
   }

   private boolean notSpecialField(Field field) {
      return !specialFields.contains(field.getName());
   }

   private void autoInject(Object source) throws Exception {
      ensureTargetExists();
      Set<Field> targetFields = getAllFields(target.getClass());
      for (Field field : getAllFields(source.getClass()))
         if (!callSetterIfExists(source, field))
            setFieldIfExists(source, targetFields, field);
   }

   private void ensureTargetExists() {
      if (target == null)
         throw new RuntimeException("Target value is null, did you forget to create it?");
   }

   private boolean callSetterIfExists(Object source, Field field) throws Exception {
      Method method = getMethod(target, getSetterName(field));
      if (method != null) {
         method.invoke(target, getFieldValue(source, field));
         return true;
      }
      return false;
   }

   private Method getMethod(Object target, String setterName) {
      for (Method method : target.getClass().getMethods())
         if (method.getName().equals(setterName))
            return method;
      return null;
   }

   private String getSetterName(Field field) {
      return "set" + StringUtils.capitalize(field.getName());
   }

   private void setFieldIfExists(Object source, Set<Field> targetFields, Field field) throws Exception {
      Field destField = getField(field.getName(), targetFields);
      if (destField != null)
         setField(target, destField, getFieldValue(source, field));
   }

   private Field getField(String name, Set<Field> fields) {
      for (Field field : fields)
         if (field.getName().equals(name))
            return field;
      return null;
   }

   private void setField(Object target, Field destField, Object fieldValue) throws Exception {
      destField.setAccessible(true);
      destField.set(target, fieldValue);
   }
}

2 comments:

  1. Why not just use @InjectMocks from Mockito?

    ReplyDelete
  2. For one, I didn't know about it. :)

    But I needed a way to do constructor injection, and it doesn't look like that supports it, yet. Looks promising though. I always enjoy deleting code I wrote that isn't necessary any more.

    ReplyDelete