    001    package org.junit.experimental.theories;
    002
    003    import java.lang.reflect.Constructor;
    004    import java.lang.reflect.Field;
    005    import java.lang.reflect.Method;
    006    import java.lang.reflect.Modifier;
    007    import java.util.ArrayList;
    008    import java.util.List;
    009
    010    import org.junit.Assert;
    011    import org.junit.Assume;
    012    import org.junit.experimental.theories.internal.Assignments;
    013    import org.junit.experimental.theories.internal.ParameterizedAssertionError;
    014    import org.junit.internal.AssumptionViolatedException;
    015    import org.junit.runners.BlockJUnit4ClassRunner;
    016    import org.junit.runners.model.FrameworkMethod;
    017    import org.junit.runners.model.InitializationError;
    018    import org.junit.runners.model.Statement;
    019    import org.junit.runners.model.TestClass;
    020
    021    /**
    022     * The Theories runner allows to test a certain functionality against a subset of an infinite set of data points.
    023     * <p>
    024     * A Theory is a piece of functionality (a method) that is executed against several data inputs called data points.
    025     * To make a test method a theory you mark it with <b>&#064;Theory</b>. To create a data point you create a public
    026     * field in your test class and mark it with <b>&#064;DataPoint</b>. The Theories runner then executes your test
    027     * method as many times as the number of data points declared, providing a different data point as
    028     * the input argument on each invocation.
    029     * </p>
    030     * <p>
    031     * A Theory differs from standard test method in that it captures some aspect of the intended behavior in possibly
    032     * infinite numbers of scenarios which corresponds to the number of data points declared. Using assumptions and
    033     * assertions properly together with covering multiple scenarios with different data points can make your tests more
    034     * flexible and bring them closer to scientific theories (hence the name).
    035     * </p>
    036     * <p>
    037     * For example:
    038     * <pre>
    039     *
    040     * &#064;RunWith(<b>Theories.class</b>)
    041     * public class UserTest {
    042     *      <b>&#064;DataPoint</b>
    043     *      public static String GOOD_USERNAME = "optimus";
    044     *      <b>&#064;DataPoint</b>
    045     *      public static String USERNAME_WITH_SLASH = "optimus/prime";
    046     *
    047     *      <b>&#064;Theory</b>
    048     *      public void filenameIncludesUsername(String username) {
    049     *          assumeThat(username, not(containsString("/")));
    050     *          assertThat(new User(username).configFileName(), containsString(username));
    051     *      }
    052     * }
    053     * </pre>
    054     * This makes it clear that the username should be included in the config file name,
    055     * only if it doesn't contain a slash. Another test or theory might define what happens when a username does contain
    056     * a slash. <code>UserTest</code> will attempt to run <code>filenameIncludesUsername</code> on every compatible data
    057     * point defined in the class. If any of the assumptions fail, the data point is silently ignored. If all of the
    058     * assumptions pass, but an assertion fails, the test fails. If no parameters can be found that satisfy all assumptions, the test fails.
    059     * <p>
    060     * Defining general statements as theories allows data point reuse across a bunch of functionality tests and also
    061     * allows automated tools to search for new, unexpected data points that expose bugs.
    062     * </p>
    063     * <p>
    064     * The support for Theories has been absorbed from the Popper project, and more complete documentation can be found
    065     * from that projects archived documentation.
    066     * </p>
    067     *
    068     * @see <a href="http://web.archive.org/web/20071012143326/popper.tigris.org/tutorial.html">Archived Popper project documentation</a>
    069     * @see <a href="http://web.archive.org/web/20110608210825/http://shareandenjoy.saff.net/tdd-specifications.pdf">Paper on Theories</a>
    070     */
    071    public class Theories extends BlockJUnit4ClassRunner {
    072        public Theories(Class<?> klass) throws InitializationError {
    073            super(klass);
    074        }
    075
    076        /** @since 4.13 */
    077        protected Theories(TestClass testClass) throws InitializationError {
    078            super(testClass);
    079        }
    080
    081        @Override
    082        protected void collectInitializationErrors(List<Throwable> errors) {
    083            super.collectInitializationErrors(errors);
    084            validateDataPointFields(errors);
    085            validateDataPointMethods(errors);
    086        }
    087
    088        private void validateDataPointFields(List<Throwable> errors) {
    089            Field[] fields = getTestClass().getJavaClass().getDeclaredFields();
    090
    091            for (Field field : fields) {
    092                if (field.getAnnotation(DataPoint.class) == null && field.getAnnotation(DataPoints.class) == null) {
    093                    continue;
    094                }
    095                if (!Modifier.isStatic(field.getModifiers())) {
    096                    errors.add(new Error("DataPoint field " + field.getName() + " must be static"));
    097                }
    098                if (!Modifier.isPublic(field.getModifiers())) {
    099                    errors.add(new Error("DataPoint field " + field.getName() + " must be public"));
    100                }
    101            }
    102        }
    103
    104        private void validateDataPointMethods(List<Throwable> errors) {
    105            Method[] methods = getTestClass().getJavaClass().getDeclaredMethods();
    106
    107            for (Method method : methods) {
    108                if (method.getAnnotation(DataPoint.class) == null && method.getAnnotation(DataPoints.class) == null) {
    109                    continue;
    110                }
    111                if (!Modifier.isStatic(method.getModifiers())) {
    112                    errors.add(new Error("DataPoint method " + method.getName() + " must be static"));
    113                }
    114                if (!Modifier.isPublic(method.getModifiers())) {
    115                    errors.add(new Error("DataPoint method " + method.getName() + " must be public"));
    116                }
    117            }
    118        }
    119
    120        @Override
    121        protected void validateConstructor(List<Throwable> errors) {
    122            validateOnlyOneConstructor(errors);
    123        }
    124
    125        @Override
    126        protected void validateTestMethods(List<Throwable> errors) {
    127            for (FrameworkMethod each : computeTestMethods()) {
    128                if (each.getAnnotation(Theory.class) != null) {
    129                    each.validatePublicVoid(false, errors);
    130                    each.validateNoTypeParametersOnArgs(errors);
    131                } else {
    132                    each.validatePublicVoidNoArg(false, errors);
    133                }
    134
    135                for (ParameterSignature signature : ParameterSignature.signatures(each.getMethod())) {
    136                    ParametersSuppliedBy annotation = signature.findDeepAnnotation(ParametersSuppliedBy.class);
    137                    if (annotation != null) {
    138                        validateParameterSupplier(annotation.value(), errors);
    139                    }
    140                }
    141            }
    142        }
    143
    144        private void validateParameterSupplier(Class<? extends ParameterSupplier> supplierClass, List<Throwable> errors) {
    145            Constructor<?>[] constructors = supplierClass.getConstructors();
    146
    147            if (constructors.length != 1) {
    148                errors.add(new Error("ParameterSupplier " + supplierClass.getName() +
    149                                     " must have only one constructor (either empty or taking only a TestClass)"));
    150            } else {
    151                Class<?>[] paramTypes = constructors[0].getParameterTypes();
    152                if (!(paramTypes.length == 0) && !paramTypes[0].equals(TestClass.class)) {
    153                    errors.add(new Error("ParameterSupplier " + supplierClass.getName() +
    154                                         " constructor must take either nothing or a single TestClass instance"));
    155                }
    156            }
    157        }
    158
    159        @Override
    160        protected List<FrameworkMethod> computeTestMethods() {
    161            List<FrameworkMethod> testMethods = new ArrayList<FrameworkMethod>(super.computeTestMethods());
    162            List<FrameworkMethod> theoryMethods = getTestClass().getAnnotatedMethods(Theory.class);
    163            testMethods.removeAll(theoryMethods);
    164            testMethods.addAll(theoryMethods);
    165            return testMethods;
    166        }
    167
    168        @Override
    169        public Statement methodBlock(final FrameworkMethod method) {
    170            return new TheoryAnchor(method, getTestClass());
    171        }
    172
    173        public static class TheoryAnchor extends Statement {
    174            private int successes = 0;
    175
    176            private final FrameworkMethod testMethod;
    177            private final TestClass testClass;
    178
    179            private List<AssumptionViolatedException> fInvalidParameters = new ArrayList<AssumptionViolatedException>();
    180
    181            public TheoryAnchor(FrameworkMethod testMethod, TestClass testClass) {
    182                this.testMethod = testMethod;
    183                this.testClass = testClass;
    184            }
    185
    186            private TestClass getTestClass() {
    187                return testClass;
    188            }
    189
    190            @Override
    191            public void evaluate() throws Throwable {
    192                runWithAssignment(Assignments.allUnassigned(
    193                        testMethod.getMethod(), getTestClass()));
    194
    195                //if this test method is not annotated with Theory, then no successes is a valid case
    196                boolean hasTheoryAnnotation = testMethod.getAnnotation(Theory.class) != null;
    197                if (successes == 0 && hasTheoryAnnotation) {
    198                    Assert
    199                            .fail("Never found parameters that satisfied method assumptions.  Violated assumptions: "
    200                                    + fInvalidParameters);
    201                }
    202            }
    203
    204            protected void runWithAssignment(Assignments parameterAssignment)
    205                    throws Throwable {
    206                if (!parameterAssignment.isComplete()) {
    207                    runWithIncompleteAssignment(parameterAssignment);
    208                } else {
    209                    runWithCompleteAssignment(parameterAssignment);
    210                }
    211            }
    212
    213            protected void runWithIncompleteAssignment(Assignments incomplete)
    214                    throws Throwable {
    215                for (PotentialAssignment source : incomplete
    216                        .potentialsForNextUnassigned()) {
    217                    runWithAssignment(incomplete.assignNext(source));
    218                }
    219            }
    220
    221            protected void runWithCompleteAssignment(final Assignments complete)
    222                    throws Throwable {
    223                new BlockJUnit4ClassRunner(getTestClass()) {
    224                    @Override
    225                    protected void collectInitializationErrors(
    226                            List<Throwable> errors) {
    227                        // do nothing
    228                    }
    229
    230                    @Override
    231                    public Statement methodBlock(FrameworkMethod method) {
    232                        final Statement statement = super.methodBlock(method);
    233                        return new Statement() {
    234                            @Override
    235                            public void evaluate() throws Throwable {
    236                                try {
    237                                    statement.evaluate();
    238                                    handleDataPointSuccess();
    239                                } catch (AssumptionViolatedException e) {
    240                                    handleAssumptionViolation(e);
    241                                } catch (Throwable e) {
    242                                    reportParameterizedError(e, complete
    243                                            .getArgumentStrings(nullsOk()));
    244                                }
    245                            }
    246
    247                        };
    248                    }
    249
    250                    @Override
    251                    protected Statement methodInvoker(FrameworkMethod method, Object test) {
    252                        return methodCompletesWithParameters(method, complete, test);
    253                    }
    254
    255                    @Override
    256                    public Object createTest() throws Exception {
    257                        Object[] params = complete.getConstructorArguments();
    258
    259                        if (!nullsOk()) {
    260                            Assume.assumeNotNull(params);
    261                        }
    262
    263                        return getTestClass().getOnlyConstructor().newInstance(params);
    264                    }
    265                }.methodBlock(testMethod).evaluate();
    266            }
    267
    268            private Statement methodCompletesWithParameters(
    269                    final FrameworkMethod method, final Assignments complete, final Object freshInstance) {
    270                return new Statement() {
    271                    @Override
    272                    public void evaluate() throws Throwable {
    273                        final Object[] values = complete.getMethodArguments();
    274
    275                        if (!nullsOk()) {
    276                            Assume.assumeNotNull(values);
    277                        }
    278
    279                        method.invokeExplosively(freshInstance, values);
    280                    }
    281                };
    282            }
    283
    284            protected void handleAssumptionViolation(AssumptionViolatedException e) {
    285                fInvalidParameters.add(e);
    286            }
    287
    288            protected void reportParameterizedError(Throwable e, Object... params)
    289                    throws Throwable {
    290                if (params.length == 0) {
    291                    throw e;
    292                }
    293                throw new ParameterizedAssertionError(e, testMethod.getName(),
    294                        params);
    295            }
    296
    297            private boolean nullsOk() {
    298                Theory annotation = testMethod.getMethod().getAnnotation(
    299                        Theory.class);
    300                if (annotation == null) {
    301                    return false;
    302                }
    303                return annotation.nullsAccepted();
    304            }
    305
    306            protected void handleDataPointSuccess() {
    307                successes++;
    308            }
    309        }
    310    }