com.google.common.testing.AbstractPackageSanityTests Maven / Gradle / Ivy
Show all versions of guava-testlib Show documentation
/*
* Copyright (C) 2012 The Guava Authors
*
* 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.common.testing;
import static com.google.common.base.Predicates.and;
import static com.google.common.base.Predicates.not;
import static com.google.common.testing.AbstractPackageSanityTests.Chopper.suffix;
import com.google.common.annotations.GwtIncompatible;
import com.google.common.annotations.J2ktIncompatible;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.common.reflect.ClassPath;
import com.google.common.testing.NullPointerTester.Visibility;
import com.google.j2objc.annotations.J2ObjCIncompatible;
import java.io.IOException;
import java.io.Serializable;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import junit.framework.TestCase;
import org.junit.Test;
/**
* Automatically runs sanity checks against top level classes in the same package of the test that
* extends {@code AbstractPackageSanityTests}. Currently sanity checks include {@link
* NullPointerTester}, {@link EqualsTester} and {@link SerializableTester}. For example:
*
*
* public class PackageSanityTests extends AbstractPackageSanityTests {}
*
*
* Note that only top-level classes with either a non-private constructor or a non-private static
* factory method to construct instances can have their instance methods checked. For example:
*
*
* public class Address {
* private final String city;
* private final String state;
* private final String zipcode;
*
* public Address(String city, String state, String zipcode) {...}
*
* {@literal @Override} public boolean equals(Object obj) {...}
* {@literal @Override} public int hashCode() {...}
* ...
* }
*
*
* No cascading checks are performed against the return values of methods unless the method is a
* static factory method. Neither are semantics of mutation methods such as {@code
* someList.add(obj)} checked. For more detailed discussion of supported and unsupported cases, see
* {@link #testEquals}, {@link #testNulls} and {@link #testSerializable}.
*
*
For testing against the returned instances from a static factory class, such as
*
*
* interface Book {...}
* public class Books {
* public static Book hardcover(String title) {...}
* public static Book paperback(String title) {...}
* }
*
*
* please use {@link ClassSanityTester#forAllPublicStaticMethods}.
*
*
If not all classes on the classpath should be covered, {@link #ignoreClasses} can be used to
* exclude certain classes. As a special case, classes with an underscore in the name (like {@code
* AutoValue_Foo}) can be excluded using ignoreClasses({@link #UNDERSCORE_IN_NAME})
.
*
*
{@link #setDefault} allows subclasses to specify default values for types.
*
*
This class incurs IO because it scans the classpath and reads classpath resources.
*
* @author Ben Yu
* @since 14.0
*/
// TODO: Switch to JUnit 4 and use @Parameterized and @BeforeClass
// Note: @Test annotations are deliberate, as some subclasses specify @RunWith(JUnit4).
@GwtIncompatible
@J2ktIncompatible
@J2ObjCIncompatible // com.google.common.reflect.ClassPath
@ElementTypesAreNonnullByDefault
public abstract class AbstractPackageSanityTests extends TestCase {
/**
* A predicate that matches classes with an underscore in the class name. This can be used with
* {@link #ignoreClasses} to exclude generated classes, such as the {@code AutoValue_Foo} classes
* generated by AutoValue.
*
* @since 19.0
*/
public static final Predicate> UNDERSCORE_IN_NAME =
(Class> c) -> c.getSimpleName().contains("_");
/* The names of the expected method that tests null checks. */
private static final ImmutableList NULL_TEST_METHOD_NAMES =
ImmutableList.of(
"testNulls", "testNull",
"testNullPointers", "testNullPointer",
"testNullPointerExceptions", "testNullPointerException");
/* The names of the expected method that tests serializable. */
private static final ImmutableList SERIALIZABLE_TEST_METHOD_NAMES =
ImmutableList.of(
"testSerializable", "testSerialization",
"testEqualsAndSerializable", "testEqualsAndSerialization");
/* The names of the expected method that tests equals. */
private static final ImmutableList EQUALS_TEST_METHOD_NAMES =
ImmutableList.of(
"testEquals",
"testEqualsAndHashCode",
"testEqualsAndSerializable",
"testEqualsAndSerialization",
"testEquality");
private static final Chopper TEST_SUFFIX =
suffix("Test").or(suffix("Tests")).or(suffix("TestCase")).or(suffix("TestSuite"));
private final Logger logger = Logger.getLogger(getClass().getName());
private final ClassSanityTester tester = new ClassSanityTester();
private Visibility visibility = Visibility.PACKAGE;
private Predicate> classFilter =
(Class> cls) -> visibility.isVisible(cls.getModifiers());
/**
* Restricts the sanity tests for public API only. By default, package-private API are also
* covered.
*/
protected final void publicApiOnly() {
visibility = Visibility.PUBLIC;
}
/**
* Tests all top-level {@link Serializable} classes in the package. For a serializable Class
* {@code C}:
*
*
* - If {@code C} explicitly implements {@link Object#equals}, the deserialized instance will
* be checked to be equal to the instance before serialization.
*
- If {@code C} doesn't explicitly implement {@code equals} but instead inherits it from a
* superclass, no equality check is done on the deserialized instance because it's not clear
* whether the author intended for the class to be a value type.
*
- If a constructor or factory method takes a parameter whose type is interface, a dynamic
* proxy will be passed to the method. It's possible that the method body expects an
* instance method of the passed-in proxy to be of a certain value yet the proxy isn't aware
* of the assumption, in which case the equality check before and after serialization will
* fail.
*
- If the constructor or factory method takes a parameter that {@link
* AbstractPackageSanityTests} doesn't know how to construct, the test will fail.
*
- If there is no visible constructor or visible static factory method declared by {@code
* C}, {@code C} is skipped for serialization test, even if it implements {@link
* Serializable}.
*
- Serialization test is not performed on method return values unless the method is a
* visible static factory method whose return type is {@code C} or {@code C}'s subtype.
*
*
* In all cases, if {@code C} needs custom logic for testing serialization, you can add an
* explicit {@code testSerializable()} test in the corresponding {@code CTest} class, and {@code
* C} will be excluded from automated serialization test performed by this method.
*/
@Test
public void testSerializable() throws Exception {
// TODO: when we use @BeforeClass, we can pay the cost of class path scanning only once.
for (Class> classToTest :
findClassesToTest(loadClassesInPackage(), SERIALIZABLE_TEST_METHOD_NAMES)) {
if (Serializable.class.isAssignableFrom(classToTest)) {
try {
Object instance = tester.instantiate(classToTest);
if (instance != null) {
if (isEqualsDefined(classToTest)) {
SerializableTester.reserializeAndAssert(instance);
} else {
SerializableTester.reserialize(instance);
}
}
} catch (Throwable e) {
throw sanityError(classToTest, SERIALIZABLE_TEST_METHOD_NAMES, "serializable test", e);
}
}
}
}
/**
* Performs {@link NullPointerTester} checks for all top-level classes in the package. For a class
* {@code C}
*
*
* - All visible static methods are checked such that passing null for any parameter that's
* not annotated nullable (according to the rules of {@link NullPointerTester}) should throw
* {@link NullPointerException}.
*
- If there is any visible constructor or visible static factory method declared by the
* class, all visible instance methods will be checked too using the instance created by
* invoking the constructor or static factory method.
*
- If the constructor or factory method used to construct instance takes a parameter that
* {@link AbstractPackageSanityTests} doesn't know how to construct, the test will fail.
*
- If there is no visible constructor or visible static factory method declared by {@code
* C}, instance methods are skipped for nulls test.
*
- Nulls test is not performed on method return values unless the method is a visible static
* factory method whose return type is {@code C} or {@code C}'s subtype.
*
*
* In all cases, if {@code C} needs custom logic for testing nulls, you can add an explicit
* {@code testNulls()} test in the corresponding {@code CTest} class, and {@code C} will be
* excluded from the automated null tests performed by this method.
*/
@Test
public void testNulls() throws Exception {
for (Class> classToTest : findClassesToTest(loadClassesInPackage(), NULL_TEST_METHOD_NAMES)) {
try {
tester.doTestNulls(classToTest, visibility);
} catch (Throwable e) {
throw sanityError(classToTest, NULL_TEST_METHOD_NAMES, "nulls test", e);
}
}
}
/**
* Tests {@code equals()} and {@code hashCode()} implementations for every top-level class in the
* package, that explicitly implements {@link Object#equals}. For a class {@code C}:
*
*
* - The visible constructor or visible static factory method with the most parameters is used
* to construct the sample instances. In case of tie, the candidate constructors or
* factories are tried one after another until one can be used to construct sample
* instances.
*
- For the constructor or static factory method used to construct instances, it's checked
* that when equal parameters are passed, the result instance should also be equal; and vice
* versa.
*
- Inequality check is not performed against state mutation methods such as {@link
* List#add}, or functional update methods such as {@link
* com.google.common.base.Joiner#skipNulls}.
*
- If the constructor or factory method used to construct instance takes a parameter that
* {@link AbstractPackageSanityTests} doesn't know how to construct, the test will fail.
*
- If there is no visible constructor or visible static factory method declared by {@code
* C}, {@code C} is skipped for equality test.
*
- Equality test is not performed on method return values unless the method is a visible
* static factory method whose return type is {@code C} or {@code C}'s subtype.
*
*
* In all cases, if {@code C} needs custom logic for testing {@code equals()}, you can add an
* explicit {@code testEquals()} test in the corresponding {@code CTest} class, and {@code C} will
* be excluded from the automated {@code equals} test performed by this method.
*/
@Test
public void testEquals() throws Exception {
for (Class> classToTest :
findClassesToTest(loadClassesInPackage(), EQUALS_TEST_METHOD_NAMES)) {
if (!classToTest.isEnum() && isEqualsDefined(classToTest)) {
try {
tester.doTestEquals(classToTest);
} catch (Throwable e) {
throw sanityError(classToTest, EQUALS_TEST_METHOD_NAMES, "equals test", e);
}
}
}
}
/**
* Sets the default value for {@code type}, when dummy value for a parameter of the same type
* needs to be created in order to invoke a method or constructor. The default value isn't used in
* testing {@link Object#equals} because more than one sample instances are needed for testing
* inequality.
*/
protected final void setDefault(Class type, T value) {
tester.setDefault(type, value);
}
/**
* Sets two distinct values for {@code type}. These values can be used for both null pointer
* testing and equals testing.
*
* @since 17.0
*/
protected final void setDistinctValues(Class type, T value1, T value2) {
tester.setDistinctValues(type, value1, value2);
}
/** Specifies that classes that satisfy the given predicate aren't tested for sanity. */
protected final void ignoreClasses(Predicate super Class>> condition) {
this.classFilter = and(this.classFilter, not(condition));
}
private static AssertionError sanityError(
Class> cls, List explicitTestNames, String description, Throwable e) {
String message =
String.format(
Locale.ROOT,
"Error in automated %s of %s\n"
+ "If the class is better tested explicitly, you can add %s() to %sTest",
description,
cls,
explicitTestNames.get(0),
cls.getName());
return new AssertionError(message, e);
}
/**
* Finds the classes not ending with a test suffix and not covered by an explicit test whose name
* is {@code explicitTestNames}.
*/
@VisibleForTesting
List> findClassesToTest(
Iterable extends Class>> classes, Iterable explicitTestNames) {
// "a.b.Foo" -> a.b.Foo.class
TreeMap> classMap = Maps.newTreeMap();
for (Class> cls : classes) {
classMap.put(cls.getName(), cls);
}
// Foo.class -> [FooTest.class, FooTests.class, FooTestSuite.class, ...]
Multimap, Class>> testClasses = HashMultimap.create();
LinkedHashSet> candidateClasses = Sets.newLinkedHashSet();
for (Class> cls : classes) {
Optional testedClassName = TEST_SUFFIX.chop(cls.getName());
if (testedClassName.isPresent()) {
Class> testedClass = classMap.get(testedClassName.get());
if (testedClass != null) {
testClasses.put(testedClass, cls);
}
} else {
candidateClasses.add(cls);
}
}
List> result = Lists.newArrayList();
NEXT_CANDIDATE:
for (Class> candidate : Iterables.filter(candidateClasses, classFilter)) {
for (Class> testClass : testClasses.get(candidate)) {
if (hasTest(testClass, explicitTestNames)) {
// covered by explicit test
continue NEXT_CANDIDATE;
}
}
result.add(candidate);
}
return result;
}
private List> loadClassesInPackage() throws IOException {
List> classes = Lists.newArrayList();
String packageName = getClass().getPackage().getName();
for (ClassPath.ClassInfo classInfo :
ClassPath.from(getClass().getClassLoader()).getTopLevelClasses(packageName)) {
Class> cls;
try {
cls = classInfo.load();
} catch (NoClassDefFoundError e) {
// In case there were linking problems, this is probably not a class we care to test anyway.
logger.log(Level.SEVERE, "Cannot load class " + classInfo + ", skipping...", e);
continue;
}
if (!cls.isInterface()) {
classes.add(cls);
}
}
return classes;
}
private static boolean hasTest(Class> testClass, Iterable testNames) {
for (String testName : testNames) {
try {
testClass.getMethod(testName);
return true;
} catch (NoSuchMethodException e) {
continue;
}
}
return false;
}
private static boolean isEqualsDefined(Class> cls) {
try {
return !cls.getDeclaredMethod("equals", Object.class).isSynthetic();
} catch (NoSuchMethodException e) {
return false;
}
}
abstract static class Chopper {
final Chopper or(Chopper you) {
Chopper i = this;
return new Chopper() {
@Override
Optional chop(String str) {
return i.chop(str).or(you.chop(str));
}
};
}
abstract Optional chop(String str);
static Chopper suffix(String suffix) {
return new Chopper() {
@Override
Optional chop(String str) {
if (str.endsWith(suffix)) {
return Optional.of(str.substring(0, str.length() - suffix.length()));
} else {
return Optional.absent();
}
}
};
}
}
}