/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 org.apache.commons.collections; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import junit.framework.TestCase; import junit.framework.TestSuite; /** * A {@link TestCase} that can define both simple and bulk test methods. * <p> * A <I>simple test method</I> is the type of test traditionally * supplied by by {@link TestCase}. To define a simple test, create a public * no-argument method whose name starts with "test". You can specify the * the name of simple test in the constructor of <code>BulkTest</code>; * a subsequent call to {@link TestCase#run} will run that simple test. * <p> * A <I>bulk test method</I>, on the other hand, returns a new instance * of <code>BulkTest</code>, which can itself define new simple and bulk * test methods. By using the {@link #makeSuite} method, you can * automatically create a hierarchal suite of tests and child bulk tests. * <p> * For instance, consider the following two classes: * * <Pre> * public class TestSet extends BulkTest { * * private Set set; * * public TestSet(Set set) { * this.set = set; * } * * public void testContains() { * boolean r = set.contains(set.iterator().next())); * assertTrue("Set should contain first element, r); * } * * public void testClear() { * set.clear(); * assertTrue("Set should be empty after clear", set.isEmpty()); * } * } * * * public class TestHashMap extends BulkTest { * * private Map makeFullMap() { * HashMap result = new HashMap(); * result.put("1", "One"); * result.put("2", "Two"); * return result; * } * * public void testClear() { * Map map = makeFullMap(); * map.clear(); * assertTrue("Map empty after clear", map.isEmpty()); * } * * public BulkTest bulkTestKeySet() { * return new TestSet(makeFullMap().keySet()); * } * * public BulkTest bulkTestEntrySet() { * return new TestSet(makeFullMap().entrySet()); * } * } * </Pre> * * In the above examples, <code>TestSet</code> defines two * simple test methods and no bulk test methods; <code>TestHashMap</code> * defines one simple test method and two bulk test methods. When * <code>makeSuite(TestHashMap.class).run</code> is executed, * <I>five</I> simple test methods will be run, in this order:<P> * * <Ol> * <Li>TestHashMap.testClear() * <Li>TestHashMap.bulkTestKeySet().testContains(); * <Li>TestHashMap.bulkTestKeySet().testClear(); * <Li>TestHashMap.bulkTestEntrySet().testContains(); * <Li>TestHashMap.bulkTestEntrySet().testClear(); * </Ol> * * In the graphical junit test runners, the tests would be displayed in * the following tree:<P> * * <UL> * <LI>TestHashMap</LI> * <UL> * <LI>testClear * <LI>bulkTestKeySet * <UL> * <LI>testContains * <LI>testClear * </UL> * <LI>bulkTestEntrySet * <UL> * <LI>testContains * <LI>testClear * </UL> * </UL> * </UL> * * A subclass can override a superclass's bulk test by * returning <code>null</code> from the bulk test method. If you only * want to override specific simple tests within a bulk test, use the * {@link #ignoredTests} method.<P> * * Note that if you want to use the bulk test methods, you <I>must</I> * define your <code>suite()</code> method to use {@link #makeSuite}. * The ordinary {@link TestSuite} constructor doesn't know how to * interpret bulk test methods. * * @author Paul Jack * @version $Id: BulkTest.java 646780 2008-04-10 12:48:07Z niallp $ */ public class BulkTest extends TestCase implements Cloneable { // Note: BulkTest is Cloneable to make it easier to construct // BulkTest instances for simple test methods that are defined in // anonymous inner classes. Basically we don't have to worry about // finding weird constructors. (And even if we found them, technically // it'd be illegal for anyone but the outer class to invoke them). // Given one BulkTest instance, we can just clone it and reset the // method name for every simple test it defines. /** * The full name of this bulk test instance. This is the full name * that is compared to {@link #ignoredTests} to see if this * test should be ignored. It's also displayed in the text runner * to ease debugging. */ String verboseName; /** * Constructs a new <code>BulkTest</code> instance that will run the * specified simple test. * * @param name the name of the simple test method to run */ public BulkTest(String name) { super(name); this.verboseName = getClass().getName(); } /** * Creates a clone of this <code>BulkTest</code>.<P> * * @return a clone of this <code>BulkTest</code> */ public Object clone() { try { return super.clone(); } catch (CloneNotSupportedException e) { throw new Error(); // should never happen } } /** * Returns an array of test names to ignore.<P> * * If a test that's defined by this <code>BulkTest</code> or * by one of its bulk test methods has a name that's in the returned * array, then that simple test will not be executed.<P> * * A test's name is formed by taking the class name of the * root <code>BulkTest</code>, eliminating the package name, then * appending the names of any bulk test methods that were invoked * to get to the simple test, and then appending the simple test * method name. The method names are delimited by periods: * * <pre> * TestHashMap.bulkTestEntrySet.testClear * </pre> * * is the name of one of the simple tests defined in the sample classes * described above. If the sample <code>TestHashMap</code> class * included this method: * * <pre> * public String[] ignoredTests() { * return new String[] { "TestHashMap.bulkTestEntrySet.testClear" }; * } * </pre> * * then the entry set's clear method wouldn't be tested, but the key * set's clear method would. * * @return an array of the names of tests to ignore, or null if * no tests should be ignored */ public String[] ignoredTests() { return null; } /** * Returns the display name of this <code>BulkTest</code>. * * @return the display name of this <code>BulkTest</code> */ public String toString() { return getName() + "(" + verboseName + ") "; } /** * Returns a {@link TestSuite} for testing all of the simple tests * <I>and</I> all the bulk tests defined by the given class.<P> * * The class is examined for simple and bulk test methods; any child * bulk tests are also examined recursively; and the results are stored * in a hierarchal {@link TestSuite}.<P> * * The given class must be a subclass of <code>BulkTest</code> and must * not be abstract.<P> * * @param c the class to examine for simple and bulk tests * @return a {@link TestSuite} containing all the simple and bulk tests * defined by that class */ public static TestSuite makeSuite(Class c) { if (Modifier.isAbstract(c.getModifiers())) { throw new IllegalArgumentException("Class must not be abstract."); } if (!BulkTest.class.isAssignableFrom(c)) { throw new IllegalArgumentException("Class must extend BulkTest."); } return new BulkTestSuiteMaker(c).make(); } } // It was easier to use a separate class to do all the reflection stuff // for making the TestSuite instances. Having permanent state around makes // it easier to handle the recursion. class BulkTestSuiteMaker { /** The class that defines simple and bulk tests methods. */ private Class startingClass; /** List of ignored simple test names. */ private List ignored; /** The TestSuite we're currently populating. Can change over time. */ private TestSuite result; /** * The prefix for simple test methods. Used to check if a test is in * the ignored list. */ private String prefix; /** * Constructor. * * @param startingClass the starting class */ public BulkTestSuiteMaker(Class startingClass) { this.startingClass = startingClass; } /** * Makes a hierarchal TestSuite based on the starting class. * * @return the hierarchal TestSuite for startingClass */ public TestSuite make() { this.result = new TestSuite(); this.prefix = getBaseName(startingClass); result.setName(prefix); BulkTest bulk = makeFirstTestCase(startingClass); ignored = new ArrayList(); String[] s = bulk.ignoredTests(); if (s != null) { ignored.addAll(Arrays.asList(s)); } make(bulk); return result; } /** * Appends all the simple tests and bulk tests defined by the given * instance's class to the current TestSuite. * * @param bulk An instance of the class that defines simple and bulk * tests for us to append */ void make(BulkTest bulk) { Class c = bulk.getClass(); Method[] all = c.getMethods(); for (int i = 0; i < all.length; i++) { if (isTest(all[i])) addTest(bulk, all[i]); if (isBulk(all[i])) addBulk(bulk, all[i]); } } /** * Adds the simple test defined by the given method to the TestSuite. * * @param bulk The instance of the class that defined the method * (I know it's weird. But the point is, we can clone the instance * and not have to worry about constructors.) * @param m The simple test method */ void addTest(BulkTest bulk, Method m) { BulkTest bulk2 = (BulkTest)bulk.clone(); bulk2.setName(m.getName()); bulk2.verboseName = prefix + "." + m.getName(); if (ignored.contains(bulk2.verboseName)) return; result.addTest(bulk2); } /** * Adds a whole new suite of tests that are defined by the result of * the given bulk test method. In other words, the given bulk test * method is invoked, and the resulting BulkTest instance is examined * for yet more simple and bulk tests. * * @param bulk The instance of the class that defined the method * @param m The bulk test method */ void addBulk(BulkTest bulk, Method m) { String verboseName = prefix + "." + m.getName(); if (ignored.contains(verboseName)) return; BulkTest bulk2; try { bulk2 = (BulkTest)m.invoke(bulk, (Object[]) null); if (bulk2 == null) return; } catch (InvocationTargetException ex) { ex.getTargetException().printStackTrace(); throw new Error(); // FIXME; } catch (IllegalAccessException ex) { ex.printStackTrace(); throw new Error(); // FIXME; } // Save current state on the stack. String oldPrefix = prefix; TestSuite oldResult = result; prefix = prefix + "." + m.getName(); result = new TestSuite(); result.setName(m.getName()); make(bulk2); oldResult.addTest(result); // Restore the old state prefix = oldPrefix; result = oldResult; } /** * Returns the base name of the given class. * * @param c the class * @return the name of that class, minus any package names */ private static String getBaseName(Class c) { String name = c.getName(); int p = name.lastIndexOf('.'); if (p > 0) { name = name.substring(p + 1); } return name; } // These three methods are used to create a valid BulkTest instance // from a class. private static Constructor getTestCaseConstructor(Class c) { try { return c.getConstructor(new Class[] { String.class }); } catch (NoSuchMethodException e) { throw new IllegalArgumentException(c + " must provide " + "a (String) constructor"); } } private static BulkTest makeTestCase(Class c, Method m) { Constructor con = getTestCaseConstructor(c); try { return (BulkTest)con.newInstance(new Object[] {m.getName()}); } catch (InvocationTargetException e) { e.printStackTrace(); throw new RuntimeException(); // FIXME; } catch (IllegalAccessException e) { throw new Error(); // should never occur } catch (InstantiationException e) { throw new RuntimeException(); // FIXME; } } private static BulkTest makeFirstTestCase(Class c) { Method[] all = c.getMethods(); for (int i = 0; i < all.length; i++) { if (isTest(all[i])) return makeTestCase(c, all[i]); } throw new IllegalArgumentException(c.getName() + " must provide " + " at least one test method."); } /** * Returns true if the given method is a simple test method. */ private static boolean isTest(Method m) { if (!m.getName().startsWith("test")) return false; if (m.getReturnType() != Void.TYPE) return false; if (m.getParameterTypes().length != 0) return false; int mods = m.getModifiers(); if (Modifier.isStatic(mods)) return false; if (Modifier.isAbstract(mods)) return false; return true; } /** * Returns true if the given method is a bulk test method. */ private static boolean isBulk(Method m) { if (!m.getName().startsWith("bulkTest")) return false; if (m.getReturnType() != BulkTest.class) return false; if (m.getParameterTypes().length != 0) return false; int mods = m.getModifiers(); if (Modifier.isStatic(mods)) return false; if (Modifier.isAbstract(mods)) return false; return true; } }