AbstractPackageSanityTests.java revision 7dd252788645e940eada959bdde927426e2531c9
1/*
2 * Copyright (C) 2012 The Guava Authors
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.google.common.testing;
18
19import static com.google.common.base.Predicates.and;
20import static com.google.common.base.Predicates.not;
21import static com.google.common.testing.AbstractPackageSanityTests.Chopper.suffix;
22
23import com.google.common.annotations.Beta;
24import com.google.common.annotations.VisibleForTesting;
25import com.google.common.base.Optional;
26import com.google.common.base.Predicate;
27import com.google.common.collect.HashMultimap;
28import com.google.common.collect.ImmutableList;
29import com.google.common.collect.Iterables;
30import com.google.common.collect.Lists;
31import com.google.common.collect.Maps;
32import com.google.common.collect.Multimap;
33import com.google.common.collect.Sets;
34import com.google.common.reflect.ClassPath;
35import com.google.common.testing.NullPointerTester.Visibility;
36
37import junit.framework.AssertionFailedError;
38import junit.framework.TestCase;
39
40import org.junit.Test;
41
42import java.io.IOException;
43import java.io.Serializable;
44import java.util.LinkedHashSet;
45import java.util.List;
46import java.util.TreeMap;
47import java.util.logging.Level;
48import java.util.logging.Logger;
49
50/**
51 * Automatically runs sanity checks against top level classes in the same package of the test that
52 * extends {@code AbstractPackageSanityTests}. Currently sanity checks include {@link
53 * NullPointerTester}, {@link EqualsTester} and {@link SerializableTester}. For example: <pre>
54 * public class PackageSanityTests extends AbstractPackageSanityTests {}
55 * </pre>
56 *
57 * <p>Note that only top-level classes with either a non-private constructor or a non-private static
58 * factory method to construct instances can have their instance methods checked. For example: <pre>
59 * public class Address {
60 *   private final String city;
61 *   private final String state;
62 *   private final String zipcode;
63 *
64 *   public Address(String city, String state, String zipcode) {...}
65 *
66 *   {@literal @Override} public boolean equals(Object obj) {...}
67 *   {@literal @Override} public int hashCode() {...}
68 *   ...
69 * }
70 * </pre>
71 * No cascading checks are performed against the return values of methods unless the method is a
72 * static factory method. Neither are semantics of mutation methods such as {@code
73 * someList.add(obj)} checked. For more detailed discussion of supported and unsupported cases, see
74 * {@link #testEquals}, {@link #testNulls} and {@link #testSerializable}.
75 *
76 * <p>For testing against the returned instances from a static factory class, such as <pre>
77 * interface Book {...}
78 * public class Books {
79 *   public static Book hardcover(String title) {...}
80 *   public static Book paperback(String title) {...}
81 * }
82 * </pre>
83 * please use {@link ClassSanityTester#forAllPublicStaticMethods}.
84 *
85 * <p>This class incurs IO because it scans the classpath and reads classpath resources.
86 *
87 * @author Ben Yu
88 * @since 14.0
89 */
90@Beta
91// TODO: Switch to JUnit 4 and use @Parameterized and @BeforeClass
92public abstract class AbstractPackageSanityTests extends TestCase {
93
94  /* The names of the expected method that tests null checks. */
95  private static final ImmutableList<String> NULL_TEST_METHOD_NAMES = ImmutableList.of(
96      "testNulls", "testNull",
97      "testNullPointers", "testNullPointer",
98      "testNullPointerExceptions", "testNullPointerException");
99
100  /* The names of the expected method that tests serializable. */
101  private static final ImmutableList<String> SERIALIZABLE_TEST_METHOD_NAMES = ImmutableList.of(
102      "testSerializable", "testSerialization",
103      "testEqualsAndSerializable", "testEqualsAndSerialization");
104
105  /* The names of the expected method that tests equals. */
106  private static final ImmutableList<String> EQUALS_TEST_METHOD_NAMES = ImmutableList.of(
107      "testEquals", "testEqualsAndHashCode",
108      "testEqualsAndSerializable", "testEqualsAndSerialization",
109      "testEquality");
110
111  private static final Chopper TEST_SUFFIX =
112      suffix("Test")
113          .or(suffix("Tests"))
114          .or(suffix("TestCase"))
115          .or(suffix("TestSuite"));
116
117  private final Logger logger = Logger.getLogger(getClass().getName());
118  private final ClassSanityTester tester = new ClassSanityTester();
119  private Visibility visibility = Visibility.PACKAGE;
120  private Predicate<Class<?>> classFilter = new Predicate<Class<?>>() {
121    @Override public boolean apply(Class<?> cls) {
122      return visibility.isVisible(cls.getModifiers());
123    }
124  };
125
126  /**
127   * Restricts the sanity tests for public API only. By default, package-private API are also
128   * covered.
129   */
130  protected final void publicApiOnly() {
131    visibility = Visibility.PUBLIC;
132  }
133
134  /**
135   * Tests all top-level public {@link Serializable} classes in the package. For a serializable
136   * Class {@code C}:
137   * <ul>
138   * <li>If {@code C} explicitly implements {@link Object#equals}, the deserialized instance will be
139   *     checked to be equal to the instance before serialization.
140   * <li>If {@code C} doesn't explicitly implement {@code equals} but instead inherits it from a
141   *     superclass, no equality check is done on the deserialized instance because it's not clear
142   *     whether the author intended for the class to be a value type.
143   * <li>If a constructor or factory method takes a parameter whose type is interface, a dynamic
144   *     proxy will be passed to the method. It's possible that the method body expects an instance
145   *     method of the passed-in proxy to be of a certain value yet the proxy isn't aware of the
146   *     assumption, in which case the equality check before and after serialization will fail.
147   * <li>If the constructor or factory method takes a parameter that {@link
148   *     AbstractPackageSanityTests} doesn't know how to construct, the test will fail.
149   * <li>If there is no public constructor or public static factory method declared by {@code C},
150   *     {@code C} is skipped for serialization test, even if it implements {@link Serializable}.
151   * <li>Serialization test is not performed on method return values unless the method is a public
152   *     static factory method whose return type is {@code C} or {@code C}'s subtype.
153   * </ul>
154   *
155   * In all cases, if {@code C} needs custom logic for testing serialization, you can add an
156   * explicit {@code testSerializable()} test in the corresponding {@code CTest} class, and {@code
157   * C} will be excluded from automated serialization test performed by this method.
158   */
159  @Test
160  public void testSerializable() throws Exception {
161    // TODO: when we use @BeforeClass, we can pay the cost of class path scanning only once.
162    for (Class<?> classToTest
163        : findClassesToTest(loadClassesInPackage(), SERIALIZABLE_TEST_METHOD_NAMES)) {
164      if (Serializable.class.isAssignableFrom(classToTest)) {
165        try {
166          Object instance = tester.instantiate(classToTest);
167          if (instance != null) {
168            if (isEqualsDefined(classToTest)) {
169              SerializableTester.reserializeAndAssert(instance);
170            } else {
171              SerializableTester.reserialize(instance);
172            }
173          }
174        } catch (Throwable e) {
175          throw sanityError(classToTest, SERIALIZABLE_TEST_METHOD_NAMES, "serializable test", e);
176        }
177      }
178    }
179  }
180
181  /**
182   * Performs {@link NullPointerTester} checks for all top-level public classes in the package. For
183   * a class {@code C}
184   * <ul>
185   * <li>All public static methods are checked such that passing null for any parameter that's not
186   *     annotated with {@link javax.annotation.Nullable} should throw {@link NullPointerException}.
187   * <li>If there is any public constructor or public static factory method declared by the class,
188   *     all public instance methods will be checked too using the instance created by invoking the
189   *     constructor or static factory method.
190   * <li>If the constructor or factory method used to construct instance takes a parameter that
191   *     {@link AbstractPackageSanityTests} doesn't know how to construct, the test will fail.
192   * <li>If there is no public constructor or public static factory method declared by {@code C},
193   *     instance methods are skipped for nulls test.
194   * <li>Nulls test is not performed on method return values unless the method is a public static
195   *     factory method whose return type is {@code C} or {@code C}'s subtype.
196   * </ul>
197   *
198   * In all cases, if {@code C} needs custom logic for testing nulls, you can add an explicit {@code
199   * testNulls()} test in the corresponding {@code CTest} class, and {@code C} will be excluded from
200   * the automated null tests performed by this method.
201   */
202  @Test
203  public void testNulls() throws Exception {
204    for (Class<?> classToTest
205        : findClassesToTest(loadClassesInPackage(), NULL_TEST_METHOD_NAMES)) {
206      try {
207        tester.doTestNulls(classToTest, visibility);
208      } catch (Throwable e) {
209        throw sanityError(classToTest, NULL_TEST_METHOD_NAMES, "nulls test", e);
210      }
211    }
212  }
213
214  /**
215   * Tests {@code equals()} and {@code hashCode()} implementations for every top-level public class
216   * in the package, that explicitly implements {@link Object#equals}. For a class {@code C}:
217   * <ul>
218   * <li>The public constructor or public static factory method with the most parameters is used to
219   *     construct the sample instances. In case of tie, the candidate constructors or factories are
220   *     tried one after another until one can be used to construct sample instances.
221   * <li>For the constructor or static factory method used to construct instances, it's checked that
222   *     when equal parameters are passed, the result instance should also be equal; and vice versa.
223   * <li>Inequality check is not performed against state mutation methods such as {@link List#add},
224   *     or functional update methods such as {@link com.google.common.base.Joiner#skipNulls}.
225   * <li>If the constructor or factory method used to construct instance takes a parameter that
226   *     {@link AbstractPackageSanityTests} doesn't know how to construct, the test will fail.
227   * <li>If there is no public constructor or public static factory method declared by {@code C},
228   *     {@code C} is skipped for equality test.
229   * <li>Equality test is not performed on method return values unless the method is a public static
230   *     factory method whose return type is {@code C} or {@code C}'s subtype.
231   * </ul>
232   *
233   * In all cases, if {@code C} needs custom logic for testing {@code equals()}, you can add an
234   * explicit {@code testEquals()} test in the corresponding {@code CTest} class, and {@code C} will
235   * be excluded from the automated {@code equals} test performed by this method.
236   */
237  @Test
238  public void testEquals() throws Exception {
239    for (Class<?> classToTest
240        : findClassesToTest(loadClassesInPackage(), EQUALS_TEST_METHOD_NAMES)) {
241      if (!classToTest.isEnum() && isEqualsDefined(classToTest)) {
242        try {
243          tester.doTestEquals(classToTest);
244        } catch (Throwable e) {
245          throw sanityError(classToTest, EQUALS_TEST_METHOD_NAMES, "equals test", e);
246        }
247      }
248    }
249  }
250
251  /**
252   * Sets the default value for {@code type}, when dummy value for a parameter of the same type
253   * needs to be created in order to invoke a method or constructor. The default value isn't used in
254   * testing {@link Object#equals} because more than one sample instances are needed for testing
255   * inequality.
256   */
257  protected final <T> void setDefault(Class<T> type, T value) {
258    tester.setDefault(type, value);
259  }
260
261  /** Specifies that classes that satisfy the given predicate aren't tested for sanity. */
262  protected final void ignoreClasses(Predicate<? super Class<?>> condition) {
263    this.classFilter = and(this.classFilter, not(condition));
264  }
265
266  private static AssertionFailedError sanityError(
267      Class<?> cls, List<String> explicitTestNames, String description, Throwable e) {
268    String message = String.format(
269        "Error in automated %s of %s\n"
270            + "If the class is better tested explicitly, you can add %s() to %sTest",
271        description, cls, explicitTestNames.get(0), cls.getName());
272    AssertionFailedError error = new AssertionFailedError(message);
273    error.initCause(e);
274    return error;
275  }
276
277  /**
278   * Finds the classes not ending with a test suffix and not covered by an explicit test
279   * whose name is {@code explicitTestName}.
280   */
281  @VisibleForTesting List<Class<?>> findClassesToTest(
282      Iterable<? extends Class<?>> classes, Iterable<String> explicitTestNames) {
283    // "a.b.Foo" -> a.b.Foo.class
284    TreeMap<String, Class<?>> classMap = Maps.newTreeMap();
285    for (Class<?> cls : classes) {
286      classMap.put(cls.getName(), cls);
287    }
288    // Foo.class -> [FooTest.class, FooTests.class, FooTestSuite.class, ...]
289    Multimap<Class<?>, Class<?>> testClasses = HashMultimap.create();
290    LinkedHashSet<Class<?>> candidateClasses = Sets.newLinkedHashSet();
291    for (Class<?> cls : classes) {
292      Optional<String> testedClassName = TEST_SUFFIX.chop(cls.getName());
293      if (testedClassName.isPresent()) {
294        Class<?> testedClass = classMap.get(testedClassName.get());
295        if (testedClass != null) {
296          testClasses.put(testedClass, cls);
297        }
298      } else {
299        candidateClasses.add(cls);
300      }
301    }
302    List<Class<?>> result = Lists.newArrayList();
303    NEXT_CANDIDATE: for (Class<?> candidate : Iterables.filter(candidateClasses, classFilter)) {
304      for (Class<?> testClass : testClasses.get(candidate)) {
305        if (hasTest(testClass, explicitTestNames)) {
306          // covered by explicit test
307          continue NEXT_CANDIDATE;
308        }
309      }
310      result.add(candidate);
311    }
312    return result;
313  }
314
315  private List<Class<?>> loadClassesInPackage() throws IOException {
316    List<Class<?>> classes = Lists.newArrayList();
317    String packageName = getClass().getPackage().getName();
318    for (ClassPath.ClassInfo classInfo
319        : ClassPath.from(getClass().getClassLoader()).getTopLevelClasses(packageName)) {
320      Class<?> cls;
321      try {
322        cls = classInfo.load();
323      } catch (NoClassDefFoundError e) {
324        // In case there were linking problems, this is probably not a class we care to test anyway.
325        logger.log(Level.SEVERE, "Cannot load class " + classInfo + ", skipping...", e);
326        continue;
327      }
328      if (!cls.isInterface()) {
329        classes.add(cls);
330      }
331    }
332    return classes;
333  }
334
335  private static boolean hasTest(Class<?> testClass, Iterable<String> testNames) {
336    for (String testName : testNames) {
337      try {
338        testClass.getMethod(testName);
339        return true;
340      } catch (NoSuchMethodException e) {
341        continue;
342      }
343    }
344    return false;
345  }
346
347  private static boolean isEqualsDefined(Class<?> cls) {
348    try {
349      return !cls.getDeclaredMethod("equals", Object.class).isSynthetic();
350    } catch (NoSuchMethodException e) {
351      return false;
352    }
353  }
354
355  static abstract class Chopper {
356
357    final Chopper or(final Chopper you) {
358      final Chopper i = this;
359      return new Chopper() {
360        @Override Optional<String> chop(String str) {
361          return i.chop(str).or(you.chop(str));
362        }
363      };
364    }
365
366    abstract Optional<String> chop(String str);
367
368    static Chopper suffix(final String suffix) {
369      return new Chopper() {
370        @Override Optional<String> chop(String str) {
371          if (str.endsWith(suffix)) {
372            return Optional.of(str.substring(0, str.length() - suffix.length()));
373          } else {
374            return Optional.absent();
375          }
376        }
377      };
378    }
379  }
380}
381