Automate Testing JavaBeans

by andrew | 25th October 2010
Tagged:

Java Beans
As agile processes like Continious Integration and Test Driven Development become more common-place – Unit Test coverage is getting more attention than ever from Build Management.

JavaBeans are always the let down on this front and the age old argument of whether or not to write Unit Tests for them rages on. There are some very good reasons why you shouldn’t write unit tests for JavaBeans:

  • they shouldn’t contain any business logic
  • its not an effective use of a developers time
  • they are usually generated by IDEs
  • they should be excluded from unit test coverage reports anyway

I wouldn’t bother if I had to write tests manually, unless the JavaBean was of critical importance to the application, but if I had a simple way to test them automatically I would for the following reasons:

  • they usually contain all the applications runtime data
  • maintenance phases can easily introduce bugs via quick fixes and hacks
  • cut and paste errors are commonplace
  • excluding them from test coverage is awkward and can be a hassle

Below is a class that allows you to specify which beans to test automatically, and how comprehensively, without any hassle. This simply tests that if you set a property, you get back the same value/object as expected. If you change the bean implementation, the tests still work as they use reflection.

The Testing Class

package com.macbtech.blog.testing;

import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import org.easymock.EasyMock;
import org.easymock.IMocksControl;
import org.junit.Assert;

public class JavaBeanTester {

	//Chose EasyMock framework for mocks.
	private static IMocksControl mockControl = EasyMock.createControl();

	public static <T> void test(final Class<T> clazz,
								final boolean testObjectOverrides,
								final String... skipThese) throws IntrospectionException {
		final PropertyDescriptor[] props =
									Introspector.getBeanInfo(clazz).getPropertyDescriptors();
		nextProp: for (PropertyDescriptor prop : props) {
			//Check the list of properties that we don't want to test
			for (String skipThis : skipThese) {
				if (skipThis.equals(prop.getName())) {
					continue nextProp;
				}
			}
			final Method getter = prop.getReadMethod();
			final Method setter = prop.getWriteMethod();
			if (getter != null && setter != null){
				final Class<?> returnType = getter.getReturnType();
				final Class<?>[] params = setter.getParameterTypes();
				if (params.length == 1 && params[0] == returnType){
					try{
						final Object value = buildValue(returnType);
						final T bean = clazz.newInstance();
						if (testObjectOverrides) {
							testObjectOverrides(bean);
						}
						setter.invoke(bean, value);
						final Object expectedValue = value;
						final Object actualValue = getter.invoke(bean);
						Assert.assertEquals(String.format("Failed while testing property %s",
											prop.getName()), expectedValue, actualValue );
					} catch (Exception ex){
						Assert.fail(String.format("Error testing the property %s: %s",
													prop.getName(), ex.toString()));
					}
				}
			}
		}
	}

	public static <T> void test(final Class<T> clazz) throws IntrospectionException {
		test(clazz, false, new String[0]);
	}

	public static <T> void test(final Class<T> clazz, final String... skipThese)
													throws IntrospectionException {
		test(clazz, false, skipThese);
	}

	@SuppressWarnings("unchecked")
	private static <T> void testObjectOverrides(final T bean) throws IllegalAccessException,
																	 InstantiationException {
		final T otherBean = (T) bean.getClass().newInstance();
		Assert.assertEquals("Failed equals()", bean, bean );
		Assert.assertEquals("Failed equals()", otherBean, bean );
		Assert.assertEquals("Failed hashCode()", bean.hashCode(), bean.hashCode());
		Assert.assertEquals("Failed hashCode()", otherBean.hashCode(), otherBean.hashCode());
		Assert.assertEquals("Failed full hashCode()", bean.hashCode(), otherBean.hashCode());
		Assert.assertEquals("Failed toString()", otherBean.toString(), bean.toString());
	}

	private static Object buildMockValue(Class<?> clazz){
		if (!Modifier.isFinal(clazz.getModifiers())){
			//Call your mocking framework here
			return mockControl.createMock(clazz);
		} else {
			return null;
		}
	}

	private static Object buildValue(Class<?> clazz)
								throws InstantiationException,IllegalAccessException,
										IllegalArgumentException,SecurityException,
										InvocationTargetException {
		//Try mocking framework first
		final Object mockedObject = buildMockValue(clazz);
		if (mockedObject != null){
			return mockedObject;
		}
		final Constructor<?>[] ctrs = clazz.getConstructors();
		for (Constructor<?> ctr : ctrs) {
			if (ctr.getParameterTypes().length == 0) {
				return ctr.newInstance();
			}
		}
		if (clazz == String.class) return "testvalue";
		else if (clazz.isArray()) return Array.newInstance(clazz.getComponentType(), 1);
		else if (clazz == boolean.class || clazz == Boolean.class) return true;
		else if (clazz == int.class || clazz == Integer.class) return 1;
		else if (clazz == long.class || clazz == Long.class) return 1L;
		else if (clazz == double.class || clazz == Double.class) return 1.0D;
		else if (clazz == float.class || clazz == Float.class) return 1.0F;
		else if (clazz == char.class || clazz == Character.class) return 'Y';
		else if (clazz.isEnum()) return clazz.getEnumConstants()[0];
		else {
			Assert.fail("Unable to build an instance of class " + clazz.getName());
			return null; //for the compiler
		}
	}
}

The Unit Test

@Test
public void testJavaBeans() throws IntrospectionException{
	//Simple test
	JavaBeanTester.test(MySimpleJavaBean.class);
	//More complex test - toString(), equals(), hasCode() etc
	JavaBeanTester.test(MyMoreComplexJavaBean.class,true);
	//Complex test with properties to skip when testing
	JavaBeanTester.test(MyUnconventionalJavaBean.class,true,"UntestablePropertyObject");
}

One Response to “Automate Testing JavaBeans”

  1. Jan 12th, 2011 :

    Great job, I have on the odd occasion thought about doing something similar. I will disagree slightly with you about the need to test JavaBeans, these should always be tested. Not for the getters and setters explicitly but for the hashcode and equals methods. JavaBeans should always override these methods and these methods are easy to add using eclipse (it’s just a right click to generate it).

    When I say a great start I think that this should be taken further. A couple of enhancements would be to change it so that the hashcode and equals are always tested by default. Secondly it would be really good to test scenarios where hashcode and equals should not be equal, i.e. change the value of each property on one of the beans one at a time and check that the hashcode and equals are different (remembering to change the value back after the test). I have seen many examples where a new property is added onto the bean and the hashcode or equals (if it is actually overridden) has not been updated.

    But like I said, great job.

Leave a Reply

Name (Required)

Email (Required - will not be published)

Website

Message (Required)