// Copyright 2010 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.google.android.stardroid.test.util; import com.google.android.stardroid.base.Provider; import com.google.android.stardroid.base.VisibleForTesting; import junit.framework.Assert; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Version of the {@link EqualsTester} class for testing immutable classes. * With this class, you need only specify a default and an alternative set of * arguments for each parameter in the object's constructor. This class then * automatically finds the correct constructor for the object to be tested and * constructs objects of type E for each combination of arguments (where the * arguments one-by-one set to the alternative values) and tests to ensure that * objects of type E constructed with the same arguments are equal, but objects * constructed with unequal arguments are not equal. Using this class, we can * condense the boiler plate of: * * <code><pre> * new EqualsTester() * .addEqualityGroup(new Foo(1, "a"), new Foo(1, "a")) * .addEqualityGroup(new Foo(2, "a"), new Foo(2, "a")) * .addEqualityGroup(new Foo(1, "b"), new Foo(1, "b)) * .testEquals(); * </pre></code> * * to * * <code><pre> * ImmutableEqualsTester * .of(Foo.class) * .defaultArgs(1, "a") * .alternativeArgs(2, "b") * .testEquals(); * </pre></code> * * In addition to condensing the code required to do equals testing on immutable * objects, this class reduces the risk of typos caused by excessive cut and * pasting. * * @param <E> type of the object being tested for equality * * @author Brent Bryan */ public class ImmutableEqualsTester<E> { private final Class<E> clazz; public ImmutableEqualsTester(Class<E> clazz) { this.clazz = clazz; } /** * Sets the default arguments used to construct new instances of objects of * type E. */ public AlternativeBuilder<E> defaultArgs(Object... defaultArgs) { return new AlternativeBuilder<E>(clazz, defaultArgs); } /** * Convenience constructor for {@link ImmutableEqualsTester}s so that the type * doesn't need to be repeated upon construction. */ public static <F> ImmutableEqualsTester<F> of(Class<F> clazz) { return new ImmutableEqualsTester<F>(clazz); } /** * EDSL class for the step where we need to set the alternative arguments for * the equality test. * * @param <F> type of object begin tested for equality */ public static class AlternativeBuilder<F> { private final Class<F> clazz; private final Object[] defaultArgs; private AlternativeBuilder(Class<F> clazz, Object[] defaultArgs) { this.clazz = clazz; this.defaultArgs = defaultArgs; } /** * Sets the alternative arguments used to construct new instances of objects * of type E. */ public TestableBuilder<F> alternativeArgs(Object... args) { return new TestableBuilder<F>(clazz, defaultArgs, args); } } /** * EDSL class for the step where we perform the equality test. * * @param <F> type of object begin tested for equality */ public static class TestableBuilder<F> { /** Map between primitive types and their object class counterparts */ private static final Map<Class<?>, Class<?>> primitiveTypeMap = new HashMap<Class<?>, Class<?>>(); static { primitiveTypeMap.put(Byte.TYPE, Byte.class); primitiveTypeMap.put(Short.TYPE, Short.class); primitiveTypeMap.put(Integer.TYPE, Integer.class); primitiveTypeMap.put(Long.TYPE, Long.class); primitiveTypeMap.put(Float.TYPE, Float.class); primitiveTypeMap.put(Double.TYPE, Double.class); primitiveTypeMap.put(Boolean.TYPE, Boolean.class); primitiveTypeMap.put(Character.TYPE, Character.class); } private final Class<F> clazz; private final Object[] defaultArgs; private final Object[] alternateArgs; private Provider<EqualsTester> equalsTesterProvider; private TestableBuilder(Class<F> clazz, Object[] defaultArgs, Object[] alternativeArgs) { this.clazz = clazz; this.defaultArgs = defaultArgs; this.alternateArgs = alternativeArgs; this.equalsTesterProvider = new Provider<EqualsTester>() { @Override public EqualsTester get() { return new EqualsTester(); } }; } /** * Sets the {@link Provider} of {@link EqualsTester}s used in the * {@link #testEquals} method to the given object. Used during testing to * mock out the {@link EqualsTester} dependency. */ @VisibleForTesting TestableBuilder<F> setEqualsTesterProvider(Provider<EqualsTester> provider) { this.equalsTesterProvider = provider; return this; } /** * Returns true if the given argument {@link Class} is of the same type as * the given type {@link Class}. Typically, this means that the function * returns true if the argument {@link Class} is a subclass of the type * {@link Class}. However, special magic is required to deal with primitive * types. */ @VisibleForTesting static boolean isArgOfType(Object arg, Class<?> type) { if (arg == null) { return Object.class.isAssignableFrom(type); } Class<?> autoBoxedType = primitiveTypeMap.get(type); if (autoBoxedType != null) { return autoBoxedType.isInstance(arg); } return type.isInstance(arg); } /** * Find all those constructors which match the types of the default * arguments. */ @VisibleForTesting static boolean areArgsCompatiableWithTypes(Object[] args, Class<?>[] types) { if (args.length != types.length) { return false; } for (int i = 0; i < args.length; i++) { if (!isArgOfType(args[i], types[i])) { return false; } } return true; } /** * Returns a {@link List} of {@link Constructor}s which are compatible with * the given set of arguments. More than one constructor may be compatible * due to overloading. */ static <T> List<Constructor<T>> getCompatibleConstructors( List<Constructor<T>> constructors, Object[] args) { List<Constructor<T>> result = new ArrayList<Constructor<T>>(); for (Constructor<T> constructor : constructors) { if (constructor.getModifiers() == Modifier.PRIVATE) { continue; } if (areArgsCompatiableWithTypes(args, constructor.getParameterTypes())) { result.add(constructor); } } return result; } /** * Creates an array of arguments where each value in the array is equal to * the value in the defaultArgs array, except for the one at the given index * value, which is equal to the value in the alternativeArgs array for that * index. If index is out of the default args bounds, then this method * returns the defaultArgs array. */ @VisibleForTesting static Object[] getArgs(Object[] defaultArgs, Object[] alternateArgs, int index) { if (index < 0 || index >= defaultArgs.length) { return defaultArgs; } Object[] args = new Object[defaultArgs.length]; System.arraycopy(defaultArgs, 0, args, 0, defaultArgs.length); args[index] = alternateArgs[index]; return args; } /** * Constructs and tests that the N + 1 possible instances (where each of the * N parameters are set to the alternative parameter one at a time) of * objects of type F are equal to other instances created with the same * arguments, but unequal to objects of the same type created with different * arguments. */ public void testEquals() { @SuppressWarnings("unchecked") List<Constructor<F>> constructors = (List) Arrays.asList(clazz.getDeclaredConstructors()); constructors = getCompatibleConstructors(constructors, defaultArgs); Assert.assertFalse(String.format( "Expected at least one constructor to match default args (%s), but found none.", Arrays.asList(defaultArgs)), constructors.isEmpty()); constructors = getCompatibleConstructors(constructors, alternateArgs); Assert.assertEquals(String.format( "Expected only one constructor to match default and alternative args, but found %d.", constructors.size()), 1, constructors.size()); Constructor<F> constructor = constructors.get(0); try { EqualsTester tester = equalsTesterProvider.get(); for (int i = 0; i <= alternateArgs.length; i++) { Object[] args = getArgs(defaultArgs, alternateArgs, i); tester.newEqualityGroup(constructor.newInstance(args), constructor.newInstance(args)); } tester.testEquals(); } catch (SecurityException e) { Assert.fail("Could not access constructor for " + clazz + " with types: " + Arrays.asList(constructor.getParameterTypes())); } catch (InstantiationException e) { Assert.fail("Failed to create a new " + clazz + " instance: " + e); } catch (IllegalAccessException e) { Assert.fail("Failed to create a new " + clazz + " instance: " + e); } catch (InvocationTargetException e) { Assert.fail("Failed to create a new " + clazz + " instance: " + e); } } } }