All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.common.testing.AbstractPackageSanityTests Maven / Gradle / Ivy

Go to download

Guava testlib is a set of java classes used for more convenient unit testing - particularly to assist the tests for Guava itself.

There is a newer version: 33.3.1-jre
Show newest version
/*
 * 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> 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> 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(); } } }; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy