package com.midwinter.junit;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Set;
import java.util.TreeSet;
/**
* Automates JUnit testing of simple getter/setter methods.
*
*
* It may be used in exclusive or inclusive mode. In exclusive mode, which
* is the default, all JavaBeans properties (getter/setter method pairs with
* matching names) are tested unless they are excluded beforehand. For
* example:
*
*
* MyClass objectToTest = new MyClass();
* GetterSetterTester gst = new GetterSetterTester(objectToTest);
* gst.exclude("complexProperty");
* gst.exclude("anotherProperty");
* gst.test();
*
*
*
* In inclusive mode, only properties that are explicitly listed are tested.
* For example:
*
*
* new GetterSetterTester(new MyClass()).
* include("aSimpleProperty").
* include("secondProperty").
* test();
*
*
*
* The second example also illustrates how to call this class in as terse a
* way as possible.
*
*
* The following property types are supported:
*
*
* - All Java primitive types.
*
- Interfaces.
*
- All non-final classes if
* cglib
* is on your classpath -- this uses cglib even when a no-argument
* constructor is available because a constructor might have side effects
* that you wouldn.t want to trigger in a unit test.
*
- Java 5 enums.
*
*
*
* Properties whose types are classes declared final
are not supported;
* neither are non-primitive, non-interface properties if you don't
* have cglib.
*
*
* Copyright (c) 2005, Steven Grimm.
* This software may be used for any purpose, commercial or noncommercial, so
* long as this copyright notice is retained. If you make improvements to the
* code, you're encouraged (but not required) to send them to me so I can
* make them available to others. For updates, please check
* here.
*
* @author Steven Grimm koreth@midwinter.com
* @version 1.0 (2005/11/08).
*/
public class GetterSetterTester
{
/** Object under test. */
private Object obj;
/** Class of object under test. */
private Class> clazz;
/** Set of fields to exclude. */
private Set excludes = new TreeSet();
/** Set of fields to include. */
private Set includes = null;
/** If true, output trace information. */
private boolean verbose = false;
/**
* Constructs a new getter/setter tester to test objects of a particular
* class.
*
* @param obj
* Object to test.
*/
public GetterSetterTester(Object obj)
{
this.obj = obj;
this.clazz = obj.getClass();
}
/**
* Adds a field to the list of tested fields. If this method is called,
* the tester will not attempt to list all the getters and setters on the
* object under test, and will instead simply test all the fields in the
* include list.
*
* @param field
* Field name whose getter/setter should be tested.
* @return
* This object, so include calls can be chained together.
*/
public GetterSetterTester include(String field)
{
if (includes == null)
includes = new TreeSet();
includes.add(field.toLowerCase());
return this;
}
/**
* Adds a field to the list of excluded fields.
*
* @param field
* Field name to exclude from testing.
* @return
* This object, so exclude calls can be chained together.
*/
public GetterSetterTester exclude(String field)
{
excludes.add(field.toLowerCase());
return this;
}
/**
* Sets the verbosity flag.
*/
public GetterSetterTester setVerbose(boolean verbose)
{
this.verbose = verbose;
return this;
}
/**
* Walks through the methods in the class looking for getters and setters
* that are on our include list (if any) and are not on our exclude list.
* @throws IllegalAccessException
* @throws IllegalArgumentException
* @throws ClassNotFoundException
* @throws SecurityException
* @throws NoSuchMethodException
* @throws InstantiationException
*/
public void test()
throws InvocationTargetException, IllegalArgumentException,
IllegalAccessException, SecurityException,
ClassNotFoundException, NoSuchMethodException, InstantiationException
{
Method[] methods = clazz.getMethods();
for (int i = 0; i < methods.length; i++)
{
/* We're looking for single-argument setters. */
Method m = methods[i];
if (! m.getName().startsWith("set"))
continue;
String fieldName = m.getName().substring(3);
Class[] args = m.getParameterTypes();
if (args.length != 1)
continue;
/* Check the field name against our include/exclude list. */
if (includes != null &&
! includes.contains(fieldName.toLowerCase()))
{
continue;
}
if (excludes.contains(fieldName.toLowerCase()))
continue;
/* Is there a getter that returns the same type? */
Method getter;
try {
getter = clazz.getMethod("get" + fieldName,
new Class[] { });
if (getter.getReturnType() != args[0])
continue;
}
catch (NoSuchMethodException e) {
continue;
}
testGetterSetter(getter, m, args[0]);
}
}
/**
* Dummy invocation handler for our proxy objects.
*/
class DummyInvocationHandler implements InvocationHandler
{
public Object invoke(Object o, Method m, Object[] a) { return null; }
}
/**
* Tests a single getter/setter pair using an argument of a particular
* type.
* @throws IllegalAccessException
* @throws IllegalArgumentException
* @throws NoSuchMethodException
* @throws ClassNotFoundException
* @throws SecurityException
* @throws InstantiationException
*/
private void testGetterSetter(Method get, Method set, Class argType)
throws InvocationTargetException, IllegalArgumentException,
IllegalAccessException, SecurityException,
ClassNotFoundException, NoSuchMethodException, InstantiationException
{
if (this.verbose)
System.out.println("Testing " + get.getDeclaringClass().getName() +
"." + get.getName());
Object proxy = makeProxy(argType);
try {
set.invoke(this.obj, new Object[] { proxy });
}
catch (InvocationTargetException e) {
throw new RuntimeException("Setter " + set.getDeclaringClass().getName() + "." + set.getName() + " threw " + e.getTargetException().toString());
}
Object getResult;
try {
getResult = get.invoke(this.obj, new Object[] { });
}
catch (InvocationTargetException e) {
throw new RuntimeException("Setter " + set.getDeclaringClass().getName() + "." + set.getName() + " threw " + e.getTargetException().toString());
}
if (getResult == proxy || proxy.equals(getResult))
return;
throw new RuntimeException("Getter " + get.getName() +
" did not return value from setter");
}
/**
* Makes a proxy of a given class. If the class is an interface type,
* uses the standard JDK proxy mechanism. If it's not, uses cglib.
* The use of cglib is via reflection so that cglib is not required to
* use this library unless the caller actually needs to proxy a
* concrete class.
* @throws NoSuchMethodException
* @throws SecurityException
* @throws InvocationTargetException
* @throws IllegalAccessException
* @throws IllegalArgumentException
*/
@SuppressWarnings("unchecked")
private Object makeProxy(Class> type)
throws ClassNotFoundException, SecurityException, NoSuchMethodException,
IllegalArgumentException, IllegalAccessException,
InvocationTargetException, InstantiationException
{
/* If it's a primitive type, just create it. */
if (type == String.class)
return "";
if (type == Integer.class || type == int.class)
return new Integer(0);
if (type == Long.class || type == long.class)
return new Long(0);
if (type == Double.class || type == double.class)
return new Double(0);
if (type == Float.class || type == float.class)
return new Float(0);
if (type == Character.class || type == char.class)
return new Character('x');
if (type == BigDecimal.class)
return new BigDecimal("0");
if (type == BigInteger.class)
return new BigInteger("0");
// JAVA5 - Comment out or remove the next two lines on older Java versions.
if (type.isEnum())
return makeEnum((Class)type);
/* Use JDK dynamic proxy if the argument is an interface. */
if (type.isInterface())
return Proxy.newProxyInstance(type.getClassLoader(),
new Class[] { type },
new DummyInvocationHandler());
/* Get the CGLib classes we need. */
Class enhancerClass = null;
Class callbackClass = null;
Class fixedValueClass = null;
try {
enhancerClass = Class.forName("net.sf.cglib.proxy.Enhancer");
callbackClass = Class.forName("net.sf.cglib.proxy.Callback");
fixedValueClass = Class.forName("net.sf.cglib.proxy.FixedValue");
}
catch (ClassNotFoundException e) {
throw new ClassNotFoundException("Need cglib to make a dummy " +
type.getName() + ". Make sure cglib.jar is on " +
"your classpath.");
}
/* Make a dummy callback (proxies within proxies!) */
Object callback;
callback = Proxy.newProxyInstance(callbackClass.getClassLoader(),
new Class[] { fixedValueClass },
new DummyInvocationHandler());
Method createMethod = enhancerClass.getMethod("create",
new Class[] { Class.class, callbackClass });
return createMethod.invoke(null, new Object[] { type, callback});
}
/**
* Returns an instance of an enum.
*
* JAVA5 - Comment out or remove this method on older Java versions.
*/
private Object makeEnum(Class clazz) throws SecurityException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException
{
Method m = clazz.getMethod("values", new Class[0]);
Object[] o = (Object[]) m.invoke(null, new Object[0]);
return o[0];
}
}