/* * Copyright (C) 2005 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.Preconditions.checkNotNull; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import junit.framework.AssertionFailedError; import junit.framework.TestCase; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.List; import java.util.Set; import javax.annotation.Nullable; /** * Unit test for {@link NullPointerTester}. * * @author Kevin Bourrillion * @author Mick Killianey */ public class NullPointerTesterTest extends TestCase { private NullPointerTester tester; @Override protected void setUp() throws Exception { super.setUp(); tester = new NullPointerTester(); } /** Non-NPE RuntimeException. */ public static class FooException extends RuntimeException { private static final long serialVersionUID = 1L; } /** * Class for testing all permutations of static/non-static one-argument * methods using methodParameter(). */ public static class OneArg { public static void staticOneArgCorrectlyThrowsNpe(String s) { checkNotNull(s); // expect NPE here on null } public static void staticOneArgThrowsOtherThanNpe(String s) { throw new FooException(); // should catch as failure } public static void staticOneArgShouldThrowNpeButDoesnt(String s) { // should catch as failure } public static void staticOneArgNullableCorrectlyDoesNotThrowNPE(@Nullable String s) { // null? no problem } public static void staticOneArgNullableCorrectlyThrowsOtherThanNPE(@Nullable String s) { throw new FooException(); // ok, as long as it's not NullPointerException } public static void staticOneArgNullableThrowsNPE(@Nullable String s) { checkNotNull(s); // doesn't check if you said you'd accept null, but you don't } public void oneArgCorrectlyThrowsNpe(String s) { checkNotNull(s); // expect NPE here on null } public void oneArgThrowsOtherThanNpe(String s) { throw new FooException(); // should catch as failure } public void oneArgShouldThrowNpeButDoesnt(String s) { // should catch as failure } public void oneArgNullableCorrectlyDoesNotThrowNPE(@Nullable String s) { // null? no problem } public void oneArgNullableCorrectlyThrowsOtherThanNPE(@Nullable String s) { throw new FooException(); // ok, as long as it's not NullPointerException } public void oneArgNullableThrowsNPE(@Nullable String s) { checkNotNull(s); // doesn't check if you said you'd accept null, but you don't } } private static final String[] STATIC_ONE_ARG_METHODS_SHOULD_PASS = { "staticOneArgCorrectlyThrowsNpe", "staticOneArgNullableCorrectlyDoesNotThrowNPE", "staticOneArgNullableCorrectlyThrowsOtherThanNPE", "staticOneArgNullableThrowsNPE", }; private static final String[] STATIC_ONE_ARG_METHODS_SHOULD_FAIL = { "staticOneArgThrowsOtherThanNpe", "staticOneArgShouldThrowNpeButDoesnt", }; private static final String[] NONSTATIC_ONE_ARG_METHODS_SHOULD_PASS = { "oneArgCorrectlyThrowsNpe", "oneArgNullableCorrectlyDoesNotThrowNPE", "oneArgNullableCorrectlyThrowsOtherThanNPE", "oneArgNullableThrowsNPE", }; private static final String[] NONSTATIC_ONE_ARG_METHODS_SHOULD_FAIL = { "oneArgThrowsOtherThanNpe", "oneArgShouldThrowNpeButDoesnt", }; public void testStaticOneArgMethodsThatShouldPass() throws Exception { for (String methodName : STATIC_ONE_ARG_METHODS_SHOULD_PASS) { Method method = OneArg.class.getMethod(methodName, String.class); try { tester.testMethodParameter(OneArg.class, method, 0); } catch (AssertionFailedError unexpected) { fail("Should not have flagged method " + methodName); } } } public void testStaticOneArgMethodsThatShouldFail() throws Exception { for (String methodName : STATIC_ONE_ARG_METHODS_SHOULD_FAIL) { Method method = OneArg.class.getMethod(methodName, String.class); boolean foundProblem = false; try { tester.testMethodParameter(OneArg.class, method, 0); } catch (AssertionFailedError expected) { foundProblem = true; } assertTrue("Should report error in method " + methodName, foundProblem); } } public void testNonStaticOneArgMethodsThatShouldPass() throws Exception { OneArg foo = new OneArg(); for (String methodName : NONSTATIC_ONE_ARG_METHODS_SHOULD_PASS) { Method method = OneArg.class.getMethod(methodName, String.class); try { tester.testMethodParameter(foo, method, 0); } catch (AssertionFailedError unexpected) { fail("Should not have flagged method " + methodName); } } } public void testNonStaticOneArgMethodsThatShouldFail() throws Exception { OneArg foo = new OneArg(); for (String methodName : NONSTATIC_ONE_ARG_METHODS_SHOULD_FAIL) { Method method = OneArg.class.getMethod(methodName, String.class); boolean foundProblem = false; try { tester.testMethodParameter(foo, method, 0); } catch (AssertionFailedError expected) { foundProblem = true; } assertTrue("Should report error in method " + methodName, foundProblem); } } /** * Class for testing all permutations of nullable/non-nullable two-argument * methods using testMethod(). * * normalNormal: two params, neither is Nullable * nullableNormal: only first param is Nullable * normalNullable: only second param is Nullable * nullableNullable: both params are Nullable */ public static class TwoArg { /** Action to take on a null param. */ public enum Action { THROW_A_NPE { @Override public void act() { throw new NullPointerException(); } }, THROW_OTHER { @Override public void act() { throw new FooException(); } }, JUST_RETURN { @Override public void act() {} }; public abstract void act(); } Action actionWhenFirstParamIsNull; Action actionWhenSecondParamIsNull; public TwoArg( Action actionWhenFirstParamIsNull, Action actionWhenSecondParamIsNull) { this.actionWhenFirstParamIsNull = actionWhenFirstParamIsNull; this.actionWhenSecondParamIsNull = actionWhenSecondParamIsNull; } /** Method that decides how to react to parameters. */ public void reactToNullParameters(Object first, Object second) { if (first == null) { actionWhenFirstParamIsNull.act(); } if (second == null) { actionWhenSecondParamIsNull.act(); } } /** Two-arg method with no Nullable params. */ public void normalNormal(String first, Integer second) { reactToNullParameters(first, second); } /** Two-arg method with the second param Nullable. */ public void normalNullable(String first, @Nullable Integer second) { reactToNullParameters(first, second); } /** Two-arg method with the first param Nullable. */ public void nullableNormal(@Nullable String first, Integer second) { reactToNullParameters(first, second); } /** Two-arg method with the both params Nullable. */ public void nullableNullable( @Nullable String first, @Nullable Integer second) { reactToNullParameters(first, second); } /** To provide sanity during debugging. */ @Override public String toString() { return String.format("Bar(%s, %s)", actionWhenFirstParamIsNull, actionWhenSecondParamIsNull); } } public void verifyBarPass(Method method, TwoArg bar) throws Exception { try { tester.testMethod(bar, method); } catch (AssertionFailedError incorrectError) { String errorMessage = String.format( "Should not have flagged method %s for %s", method.getName(), bar); assertNull(errorMessage, incorrectError); } } public void verifyBarFail(Method method, TwoArg bar) throws Exception { try { tester.testMethod(bar, method); } catch (AssertionFailedError expected) { return; // good...we wanted a failure } String errorMessage = String.format( "Should have flagged method %s for %s", method.getName(), bar); fail(errorMessage); } public void testTwoArgNormalNormal() throws Exception { Method method = TwoArg.class.getMethod( "normalNormal", String.class, Integer.class); for (TwoArg.Action first : TwoArg.Action.values()) { for (TwoArg.Action second : TwoArg.Action.values()) { TwoArg bar = new TwoArg(first, second); if (first.equals(TwoArg.Action.THROW_A_NPE) && second.equals(TwoArg.Action.THROW_A_NPE)) { verifyBarPass(method, bar); // require both params to throw NPE } else { verifyBarFail(method, bar); } } } } public void testTwoArgNormalNullable() throws Exception { Method method = TwoArg.class.getMethod( "normalNullable", String.class, Integer.class); for (TwoArg.Action first : TwoArg.Action.values()) { for (TwoArg.Action second : TwoArg.Action.values()) { TwoArg bar = new TwoArg(first, second); if (first.equals(TwoArg.Action.THROW_A_NPE)) { verifyBarPass(method, bar); // only pass if 1st param throws NPE } else { verifyBarFail(method, bar); } } } } public void testTwoArgNullableNormal() throws Exception { Method method = TwoArg.class.getMethod( "nullableNormal", String.class, Integer.class); for (TwoArg.Action first : TwoArg.Action.values()) { for (TwoArg.Action second : TwoArg.Action.values()) { TwoArg bar = new TwoArg(first, second); if (second.equals(TwoArg.Action.THROW_A_NPE)) { verifyBarPass(method, bar); // only pass if 2nd param throws NPE } else { verifyBarFail(method, bar); } } } } public void testTwoArgNullableNullable() throws Exception { Method method = TwoArg.class.getMethod( "nullableNullable", String.class, Integer.class); for (TwoArg.Action first : TwoArg.Action.values()) { for (TwoArg.Action second : TwoArg.Action.values()) { TwoArg bar = new TwoArg(first, second); verifyBarPass(method, bar); // All args nullable: anything goes! } } } /* * This next part consists of several sample classes that provide * demonstrations of conditions that cause NullPointerTester * to succeed/fail. * * Add naughty classes to failClasses to verify that NullPointerTest * raises an AssertionFailedError. * * Add acceptable classes to passClasses to verify that NullPointerTest * doesn't complain. */ /** List of classes that NullPointerTester should pass as acceptable. */ static List> failClasses = Lists.newArrayList(); /** List of classes that NullPointerTester should flag as problematic. */ static List> passClasses = Lists.newArrayList(); /** Lots of well-behaved methods. */ public static class PassObject { public static void doThrow(Object arg) { if (arg == null) { throw new FooException(); } } public void noArg() {} public void oneArg(String s) { checkNotNull(s); } public void oneNullableArg(@Nullable String s) {} public void oneNullableArgThrows(@Nullable String s) { doThrow(s); } public void twoArg(String s, Integer i) { checkNotNull(s); i.intValue(); } public void twoMixedArgs(String s, @Nullable Integer i) { checkNotNull(s); } public void twoMixedArgsThrows(String s, @Nullable Integer i) { checkNotNull(s); doThrow(i); } public void twoMixedArgs(@Nullable Integer i, String s) { checkNotNull(s); } public void twoMixedArgsThrows(@Nullable Integer i, String s) { checkNotNull(s); doThrow(i); } public void twoNullableArgs(@Nullable String s, @javax.annotation.Nullable Integer i) { } public void twoNullableArgsThrowsFirstArg( @Nullable String s, @Nullable Integer i) { doThrow(s); } public void twoNullableArgsThrowsSecondArg( @Nullable String s, @Nullable Integer i) { doThrow(i); } public static void staticOneArg(String s) { checkNotNull(s); } public static void staticOneNullableArg(@Nullable String s) { } public static void staticOneNullableArgThrows(@Nullable String s) { doThrow(s); } } static { passClasses.add(PassObject.class); } static class FailOneArgDoesntThrowNPE extends PassObject { @Override public void oneArg(String s) { // Fail: missing NPE for s } } static { failClasses.add(FailOneArgDoesntThrowNPE.class); } static class FailOneArgThrowsWrongType extends PassObject { @Override public void oneArg(String s) { doThrow(s); // Fail: throwing non-NPE exception for null s } } static { failClasses.add(FailOneArgThrowsWrongType.class); } static class PassOneNullableArgThrowsNPE extends PassObject { @Override public void oneNullableArg(@Nullable String s) { checkNotNull(s); // ok to throw NPE } } static { passClasses.add(PassOneNullableArgThrowsNPE.class); } static class FailTwoArgsFirstArgDoesntThrowNPE extends PassObject { @Override public void twoArg(String s, Integer i) { // Fail: missing NPE for s i.intValue(); } } static { failClasses.add(FailTwoArgsFirstArgDoesntThrowNPE.class); } static class FailTwoArgsFirstArgThrowsWrongType extends PassObject { @Override public void twoArg(String s, Integer i) { doThrow(s); // Fail: throwing non-NPE exception for null s i.intValue(); } } static { failClasses.add(FailTwoArgsFirstArgThrowsWrongType.class); } static class FailTwoArgsSecondArgDoesntThrowNPE extends PassObject { @Override public void twoArg(String s, Integer i) { checkNotNull(s); // Fail: missing NPE for i } } static { failClasses.add(FailTwoArgsSecondArgDoesntThrowNPE.class); } static class FailTwoArgsSecondArgThrowsWrongType extends PassObject { @Override public void twoArg(String s, Integer i) { checkNotNull(s); doThrow(i); // Fail: throwing non-NPE exception for null i } } static { failClasses.add(FailTwoArgsSecondArgThrowsWrongType.class); } static class FailTwoMixedArgsFirstArgDoesntThrowNPE extends PassObject { @Override public void twoMixedArgs(String s, @Nullable Integer i) { // Fail: missing NPE for s } } static { failClasses.add(FailTwoMixedArgsFirstArgDoesntThrowNPE.class); } static class FailTwoMixedArgsFirstArgThrowsWrongType extends PassObject { @Override public void twoMixedArgs(String s, @Nullable Integer i) { doThrow(s); // Fail: throwing non-NPE exception for null s } } static { failClasses.add(FailTwoMixedArgsFirstArgThrowsWrongType.class); } static class PassTwoMixedArgsNullableArgThrowsNPE extends PassObject { @Override public void twoMixedArgs(String s, @Nullable Integer i) { checkNotNull(s); i.intValue(); // ok to throw NPE? } } static { passClasses.add(PassTwoMixedArgsNullableArgThrowsNPE.class); } static class PassTwoMixedArgSecondNullableArgThrowsOther extends PassObject { @Override public void twoMixedArgs(String s, @Nullable Integer i) { checkNotNull(s); doThrow(i); // ok to throw non-NPE exception for null i } } static { passClasses.add(PassTwoMixedArgSecondNullableArgThrowsOther.class); } static class FailTwoMixedArgsSecondArgDoesntThrowNPE extends PassObject { @Override public void twoMixedArgs(@Nullable Integer i, String s) { // Fail: missing NPE for null s } } static { failClasses.add(FailTwoMixedArgsSecondArgDoesntThrowNPE.class); } static class FailTwoMixedArgsSecondArgThrowsWrongType extends PassObject { @Override public void twoMixedArgs(@Nullable Integer i, String s) { doThrow(s); // Fail: throwing non-NPE exception for null s } } static { failClasses.add(FailTwoMixedArgsSecondArgThrowsWrongType.class); } static class PassTwoNullableArgsFirstThrowsNPE extends PassObject { @Override public void twoNullableArgs( @Nullable String s, @Nullable Integer i) { checkNotNull(s); // ok to throw NPE? } } static { passClasses.add(PassTwoNullableArgsFirstThrowsNPE.class); } static class PassTwoNullableArgsFirstThrowsOther extends PassObject { @Override public void twoNullableArgs( @Nullable String s, @Nullable Integer i) { doThrow(s); // ok to throw non-NPE exception for null s } } static { passClasses.add(PassTwoNullableArgsFirstThrowsOther.class); } static class PassTwoNullableArgsSecondThrowsNPE extends PassObject { @Override public void twoNullableArgs( @Nullable String s, @Nullable Integer i) { i.intValue(); // ok to throw NPE? } } static { passClasses.add(PassTwoNullableArgsSecondThrowsNPE.class); } static class PassTwoNullableArgsSecondThrowsOther extends PassObject { @Override public void twoNullableArgs( @Nullable String s, @Nullable Integer i) { doThrow(i); // ok to throw non-NPE exception for null i } } static { passClasses.add(PassTwoNullableArgsSecondThrowsOther.class); } static class PassTwoNullableArgsNeitherThrowsAnything extends PassObject { @Override public void twoNullableArgs( @Nullable String s, @Nullable Integer i) { // ok to do nothing } } static { passClasses.add(PassTwoNullableArgsNeitherThrowsAnything.class); } /** Sanity check: it's easy to make typos. */ private void checkClasses(String message, List> classes) { Set> set = Sets.newHashSet(classes); for (Class clazz : classes) { if (!set.remove(clazz)) { fail(String.format("%s: %s appears twice. Typo?", message, clazz.getSimpleName())); } } } public void testDidntMakeTypoInTestCases() { checkClasses("passClass", passClasses); checkClasses("failClasses", failClasses); List> allClasses = Lists.newArrayList(passClasses); allClasses.addAll(failClasses); checkClasses("allClasses", allClasses); } public void testShouldNotFindProblemInPassClass() throws Exception { for (Class passClass : passClasses) { Object instance = passClass.newInstance(); try { tester.testAllPublicInstanceMethods(instance); } catch (AssertionFailedError e) { assertNull("Should not detect problem in " + passClass.getSimpleName(), e); } } } public void testShouldFindProblemInFailClass() throws Exception { for (Class failClass : failClasses) { Object instance = failClass.newInstance(); boolean foundProblem = false; try { tester.testAllPublicInstanceMethods(instance); } catch (AssertionFailedError e) { foundProblem = true; } assertTrue("Should detect problem in " + failClass.getSimpleName(), foundProblem); } } private static class PrivateClassWithPrivateConstructor { private PrivateClassWithPrivateConstructor(@Nullable Integer argument) {} } public void testPrivateClass() throws Exception { NullPointerTester tester = new NullPointerTester(); for (Constructor constructor : PrivateClassWithPrivateConstructor.class.getDeclaredConstructors()) { tester.testConstructor(constructor); } } private interface Foo { void doSomething(T bar, Integer baz); } private static class StringFoo implements Foo { @Override public void doSomething(String bar, Integer baz) { checkNotNull(bar); checkNotNull(baz); } } public void testBidgeMethodIgnored() throws Exception { new NullPointerTester().testAllPublicInstanceMethods(new StringFoo()); } /* * * TODO(kevinb): This is only a very small start. * Must come back and finish. * */ }