diff options
Diffstat (limited to 'src/main/java/org/junit/experimental/theories/Theories.java')
-rw-r--r-- | src/main/java/org/junit/experimental/theories/Theories.java | 458 |
1 files changed, 282 insertions, 176 deletions
diff --git a/src/main/java/org/junit/experimental/theories/Theories.java b/src/main/java/org/junit/experimental/theories/Theories.java index 82ff98b..817f553 100644 --- a/src/main/java/org/junit/experimental/theories/Theories.java +++ b/src/main/java/org/junit/experimental/theories/Theories.java @@ -1,16 +1,14 @@ -/** - * - */ package org.junit.experimental.theories; +import java.lang.reflect.Constructor; import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.List; import org.junit.Assert; -import org.junit.experimental.theories.PotentialAssignment.CouldNotGenerateValueException; +import org.junit.Assume; import org.junit.experimental.theories.internal.Assignments; import org.junit.experimental.theories.internal.ParameterizedAssertionError; import org.junit.internal.AssumptionViolatedException; @@ -20,180 +18,288 @@ import org.junit.runners.model.InitializationError; import org.junit.runners.model.Statement; import org.junit.runners.model.TestClass; +/** + * The Theories runner allows to test a certain functionality against a subset of an infinite set of data points. + * <p> + * A Theory is a piece of functionality (a method) that is executed against several data inputs called data points. + * To make a test method a theory you mark it with <b>@Theory</b>. To create a data point you create a public + * field in your test class and mark it with <b>@DataPoint</b>. The Theories runner then executes your test + * method as many times as the number of data points declared, providing a different data point as + * the input argument on each invocation. + * </p> + * <p> + * A Theory differs from standard test method in that it captures some aspect of the intended behavior in possibly + * infinite numbers of scenarios which corresponds to the number of data points declared. Using assumptions and + * assertions properly together with covering multiple scenarios with different data points can make your tests more + * flexible and bring them closer to scientific theories (hence the name). + * </p> + * <p> + * For example: + * <pre> + * + * @RunWith(<b>Theories.class</b>) + * public class UserTest { + * <b>@DataPoint</b> + * public static String GOOD_USERNAME = "optimus"; + * <b>@DataPoint</b> + * public static String USERNAME_WITH_SLASH = "optimus/prime"; + * + * <b>@Theory</b> + * public void filenameIncludesUsername(String username) { + * assumeThat(username, not(containsString("/"))); + * assertThat(new User(username).configFileName(), containsString(username)); + * } + * } + * </pre> + * This makes it clear that the user's filename should be included in the config file name, + * only if it doesn't contain a slash. Another test or theory might define what happens when a username does contain + * a slash. <code>UserTest</code> will attempt to run <code>filenameIncludesUsername</code> on every compatible data + * point defined in the class. If any of the assumptions fail, the data point is silently ignored. If all of the + * assumptions pass, but an assertion fails, the test fails. + * <p> + * Defining general statements as theories allows data point reuse across a bunch of functionality tests and also + * allows automated tools to search for new, unexpected data points that expose bugs. + * </p> + * <p> + * The support for Theories has been absorbed from the Popper project, and more complete documentation can be found + * from that projects archived documentation. + * </p> + * + * @see <a href="http://web.archive.org/web/20071012143326/popper.tigris.org/tutorial.html">Archived Popper project documentation</a> + * @see <a href="http://web.archive.org/web/20110608210825/http://shareandenjoy.saff.net/tdd-specifications.pdf">Paper on Theories</a> + */ public class Theories extends BlockJUnit4ClassRunner { - public Theories(Class<?> klass) throws InitializationError { - super(klass); - } - - @Override - protected void collectInitializationErrors(List<Throwable> errors) { - super.collectInitializationErrors(errors); - validateDataPointFields(errors); - } - - private void validateDataPointFields(List<Throwable> errors) { - Field[] fields= getTestClass().getJavaClass().getDeclaredFields(); - - for (Field each : fields) - if (each.getAnnotation(DataPoint.class) != null && !Modifier.isStatic(each.getModifiers())) - errors.add(new Error("DataPoint field " + each.getName() + " must be static")); - } - - @Override - protected void validateConstructor(List<Throwable> errors) { - validateOnlyOneConstructor(errors); - } - - @Override - protected void validateTestMethods(List<Throwable> errors) { - for (FrameworkMethod each : computeTestMethods()) - if(each.getAnnotation(Theory.class) != null) - each.validatePublicVoid(false, errors); - else - each.validatePublicVoidNoArg(false, errors); - } - - @Override - protected List<FrameworkMethod> computeTestMethods() { - List<FrameworkMethod> testMethods= super.computeTestMethods(); - List<FrameworkMethod> theoryMethods= getTestClass().getAnnotatedMethods(Theory.class); - testMethods.removeAll(theoryMethods); - testMethods.addAll(theoryMethods); - return testMethods; - } - - @Override - public Statement methodBlock(final FrameworkMethod method) { - return new TheoryAnchor(method, getTestClass()); - } - - public static class TheoryAnchor extends Statement { - private int successes= 0; - - private FrameworkMethod fTestMethod; - private TestClass fTestClass; - - private List<AssumptionViolatedException> fInvalidParameters= new ArrayList<AssumptionViolatedException>(); - - public TheoryAnchor(FrameworkMethod method, TestClass testClass) { - fTestMethod= method; - fTestClass= testClass; - } + public Theories(Class<?> klass) throws InitializationError { + super(klass); + } + + @Override + protected void collectInitializationErrors(List<Throwable> errors) { + super.collectInitializationErrors(errors); + validateDataPointFields(errors); + validateDataPointMethods(errors); + } + + private void validateDataPointFields(List<Throwable> errors) { + Field[] fields = getTestClass().getJavaClass().getDeclaredFields(); + + for (Field field : fields) { + if (field.getAnnotation(DataPoint.class) == null && field.getAnnotation(DataPoints.class) == null) { + continue; + } + if (!Modifier.isStatic(field.getModifiers())) { + errors.add(new Error("DataPoint field " + field.getName() + " must be static")); + } + if (!Modifier.isPublic(field.getModifiers())) { + errors.add(new Error("DataPoint field " + field.getName() + " must be public")); + } + } + } + + private void validateDataPointMethods(List<Throwable> errors) { + Method[] methods = getTestClass().getJavaClass().getDeclaredMethods(); + + for (Method method : methods) { + if (method.getAnnotation(DataPoint.class) == null && method.getAnnotation(DataPoints.class) == null) { + continue; + } + if (!Modifier.isStatic(method.getModifiers())) { + errors.add(new Error("DataPoint method " + method.getName() + " must be static")); + } + if (!Modifier.isPublic(method.getModifiers())) { + errors.add(new Error("DataPoint method " + method.getName() + " must be public")); + } + } + } + + @Override + protected void validateConstructor(List<Throwable> errors) { + validateOnlyOneConstructor(errors); + } + + @Override + protected void validateTestMethods(List<Throwable> errors) { + for (FrameworkMethod each : computeTestMethods()) { + if (each.getAnnotation(Theory.class) != null) { + each.validatePublicVoid(false, errors); + each.validateNoTypeParametersOnArgs(errors); + } else { + each.validatePublicVoidNoArg(false, errors); + } + + for (ParameterSignature signature : ParameterSignature.signatures(each.getMethod())) { + ParametersSuppliedBy annotation = signature.findDeepAnnotation(ParametersSuppliedBy.class); + if (annotation != null) { + validateParameterSupplier(annotation.value(), errors); + } + } + } + } + + private void validateParameterSupplier(Class<? extends ParameterSupplier> supplierClass, List<Throwable> errors) { + Constructor<?>[] constructors = supplierClass.getConstructors(); + + if (constructors.length != 1) { + errors.add(new Error("ParameterSupplier " + supplierClass.getName() + + " must have only one constructor (either empty or taking only a TestClass)")); + } else { + Class<?>[] paramTypes = constructors[0].getParameterTypes(); + if (!(paramTypes.length == 0) && !paramTypes[0].equals(TestClass.class)) { + errors.add(new Error("ParameterSupplier " + supplierClass.getName() + + " constructor must take either nothing or a single TestClass instance")); + } + } + } + + @Override + protected List<FrameworkMethod> computeTestMethods() { + List<FrameworkMethod> testMethods = new ArrayList<FrameworkMethod>(super.computeTestMethods()); + List<FrameworkMethod> theoryMethods = getTestClass().getAnnotatedMethods(Theory.class); + testMethods.removeAll(theoryMethods); + testMethods.addAll(theoryMethods); + return testMethods; + } + + @Override + public Statement methodBlock(final FrameworkMethod method) { + return new TheoryAnchor(method, getTestClass()); + } + + public static class TheoryAnchor extends Statement { + private int successes = 0; + + private final FrameworkMethod testMethod; + private final TestClass testClass; + + private List<AssumptionViolatedException> fInvalidParameters = new ArrayList<AssumptionViolatedException>(); + + public TheoryAnchor(FrameworkMethod testMethod, TestClass testClass) { + this.testMethod = testMethod; + this.testClass = testClass; + } private TestClass getTestClass() { - return fTestClass; + return testClass; } - @Override - public void evaluate() throws Throwable { - runWithAssignment(Assignments.allUnassigned( - fTestMethod.getMethod(), getTestClass())); - - if (successes == 0) - Assert - .fail("Never found parameters that satisfied method assumptions. Violated assumptions: " - + fInvalidParameters); - } - - protected void runWithAssignment(Assignments parameterAssignment) - throws Throwable { - if (!parameterAssignment.isComplete()) { - runWithIncompleteAssignment(parameterAssignment); - } else { - runWithCompleteAssignment(parameterAssignment); - } - } - - protected void runWithIncompleteAssignment(Assignments incomplete) - throws InstantiationException, IllegalAccessException, - Throwable { - for (PotentialAssignment source : incomplete - .potentialsForNextUnassigned()) { - runWithAssignment(incomplete.assignNext(source)); - } - } - - protected void runWithCompleteAssignment(final Assignments complete) - throws InstantiationException, IllegalAccessException, - InvocationTargetException, NoSuchMethodException, Throwable { - new BlockJUnit4ClassRunner(getTestClass().getJavaClass()) { - @Override - protected void collectInitializationErrors( - List<Throwable> errors) { - // do nothing - } - - @Override - public Statement methodBlock(FrameworkMethod method) { - final Statement statement= super.methodBlock(method); - return new Statement() { - @Override - public void evaluate() throws Throwable { - try { - statement.evaluate(); - handleDataPointSuccess(); - } catch (AssumptionViolatedException e) { - handleAssumptionViolation(e); - } catch (Throwable e) { - reportParameterizedError(e, complete - .getArgumentStrings(nullsOk())); - } - } - - }; - } - - @Override - protected Statement methodInvoker(FrameworkMethod method, Object test) { - return methodCompletesWithParameters(method, complete, test); - } - - @Override - public Object createTest() throws Exception { - return getTestClass().getOnlyConstructor().newInstance( - complete.getConstructorArguments(nullsOk())); - } - }.methodBlock(fTestMethod).evaluate(); - } - - private Statement methodCompletesWithParameters( - final FrameworkMethod method, final Assignments complete, final Object freshInstance) { - return new Statement() { - @Override - public void evaluate() throws Throwable { - try { - final Object[] values= complete.getMethodArguments( - nullsOk()); - method.invokeExplosively(freshInstance, values); - } catch (CouldNotGenerateValueException e) { - // ignore - } - } - }; - } - - protected void handleAssumptionViolation(AssumptionViolatedException e) { - fInvalidParameters.add(e); - } - - protected void reportParameterizedError(Throwable e, Object... params) - throws Throwable { - if (params.length == 0) - throw e; - throw new ParameterizedAssertionError(e, fTestMethod.getName(), - params); - } - - private boolean nullsOk() { - Theory annotation= fTestMethod.getMethod().getAnnotation( - Theory.class); - if (annotation == null) - return false; - return annotation.nullsAccepted(); - } - - protected void handleDataPointSuccess() { - successes++; - } - } + @Override + public void evaluate() throws Throwable { + runWithAssignment(Assignments.allUnassigned( + testMethod.getMethod(), getTestClass())); + + //if this test method is not annotated with Theory, then no successes is a valid case + boolean hasTheoryAnnotation = testMethod.getAnnotation(Theory.class) != null; + if (successes == 0 && hasTheoryAnnotation) { + Assert + .fail("Never found parameters that satisfied method assumptions. Violated assumptions: " + + fInvalidParameters); + } + } + + protected void runWithAssignment(Assignments parameterAssignment) + throws Throwable { + if (!parameterAssignment.isComplete()) { + runWithIncompleteAssignment(parameterAssignment); + } else { + runWithCompleteAssignment(parameterAssignment); + } + } + + protected void runWithIncompleteAssignment(Assignments incomplete) + throws Throwable { + for (PotentialAssignment source : incomplete + .potentialsForNextUnassigned()) { + runWithAssignment(incomplete.assignNext(source)); + } + } + + protected void runWithCompleteAssignment(final Assignments complete) + throws Throwable { + new BlockJUnit4ClassRunner(getTestClass().getJavaClass()) { + @Override + protected void collectInitializationErrors( + List<Throwable> errors) { + // do nothing + } + + @Override + public Statement methodBlock(FrameworkMethod method) { + final Statement statement = super.methodBlock(method); + return new Statement() { + @Override + public void evaluate() throws Throwable { + try { + statement.evaluate(); + handleDataPointSuccess(); + } catch (AssumptionViolatedException e) { + handleAssumptionViolation(e); + } catch (Throwable e) { + reportParameterizedError(e, complete + .getArgumentStrings(nullsOk())); + } + } + + }; + } + + @Override + protected Statement methodInvoker(FrameworkMethod method, Object test) { + return methodCompletesWithParameters(method, complete, test); + } + + @Override + public Object createTest() throws Exception { + Object[] params = complete.getConstructorArguments(); + + if (!nullsOk()) { + Assume.assumeNotNull(params); + } + + return getTestClass().getOnlyConstructor().newInstance(params); + } + }.methodBlock(testMethod).evaluate(); + } + + private Statement methodCompletesWithParameters( + final FrameworkMethod method, final Assignments complete, final Object freshInstance) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + final Object[] values = complete.getMethodArguments(); + + if (!nullsOk()) { + Assume.assumeNotNull(values); + } + + method.invokeExplosively(freshInstance, values); + } + }; + } + + protected void handleAssumptionViolation(AssumptionViolatedException e) { + fInvalidParameters.add(e); + } + + protected void reportParameterizedError(Throwable e, Object... params) + throws Throwable { + if (params.length == 0) { + throw e; + } + throw new ParameterizedAssertionError(e, testMethod.getName(), + params); + } + + private boolean nullsOk() { + Theory annotation = testMethod.getMethod().getAnnotation( + Theory.class); + if (annotation == null) { + return false; + } + return annotation.nullsAccepted(); + } + + protected void handleDataPointSuccess() { + successes++; + } + } } |