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: * *

* *

* 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]; } }