1package org.junit.experimental.theories;
2
3import java.lang.reflect.Constructor;
4import java.lang.reflect.Field;
5import java.lang.reflect.Method;
6import java.lang.reflect.Modifier;
7import java.util.ArrayList;
8import java.util.List;
9
10import org.junit.Assert;
11import org.junit.Assume;
12import org.junit.experimental.theories.internal.Assignments;
13import org.junit.experimental.theories.internal.ParameterizedAssertionError;
14import org.junit.internal.AssumptionViolatedException;
15import org.junit.runners.BlockJUnit4ClassRunner;
16import org.junit.runners.model.FrameworkMethod;
17import org.junit.runners.model.InitializationError;
18import org.junit.runners.model.Statement;
19import org.junit.runners.model.TestClass;
20
21/**
22 * The Theories runner allows to test a certain functionality against a subset of an infinite set of data points.
23 * <p>
24 * A Theory is a piece of functionality (a method) that is executed against several data inputs called data points.
25 * To make a test method a theory you mark it with <b>&#064;Theory</b>. To create a data point you create a public
26 * field in your test class and mark it with <b>&#064;DataPoint</b>. The Theories runner then executes your test
27 * method as many times as the number of data points declared, providing a different data point as
28 * the input argument on each invocation.
29 * </p>
30 * <p>
31 * A Theory differs from standard test method in that it captures some aspect of the intended behavior in possibly
32 * infinite numbers of scenarios which corresponds to the number of data points declared. Using assumptions and
33 * assertions properly together with covering multiple scenarios with different data points can make your tests more
34 * flexible and bring them closer to scientific theories (hence the name).
35 * </p>
36 * <p>
37 * For example:
38 * <pre>
39 *
40 * &#064;RunWith(<b>Theories.class</b>)
41 * public class UserTest {
42 *      <b>&#064;DataPoint</b>
43 *      public static String GOOD_USERNAME = "optimus";
44 *      <b>&#064;DataPoint</b>
45 *      public static String USERNAME_WITH_SLASH = "optimus/prime";
46 *
47 *      <b>&#064;Theory</b>
48 *      public void filenameIncludesUsername(String username) {
49 *          assumeThat(username, not(containsString("/")));
50 *          assertThat(new User(username).configFileName(), containsString(username));
51 *      }
52 * }
53 * </pre>
54 * This makes it clear that the user's filename should be included in the config file name,
55 * only if it doesn't contain a slash. Another test or theory might define what happens when a username does contain
56 * a slash. <code>UserTest</code> will attempt to run <code>filenameIncludesUsername</code> on every compatible data
57 * point defined in the class. If any of the assumptions fail, the data point is silently ignored. If all of the
58 * assumptions pass, but an assertion fails, the test fails.
59 * <p>
60 * Defining general statements as theories allows data point reuse across a bunch of functionality tests and also
61 * allows automated tools to search for new, unexpected data points that expose bugs.
62 * </p>
63 * <p>
64 * The support for Theories has been absorbed from the Popper project, and more complete documentation can be found
65 * from that projects archived documentation.
66 * </p>
67 *
68 * @see <a href="http://web.archive.org/web/20071012143326/popper.tigris.org/tutorial.html">Archived Popper project documentation</a>
69 * @see <a href="http://web.archive.org/web/20110608210825/http://shareandenjoy.saff.net/tdd-specifications.pdf">Paper on Theories</a>
70 */
71public class Theories extends BlockJUnit4ClassRunner {
72    public Theories(Class<?> klass) throws InitializationError {
73        super(klass);
74    }
75
76    @Override
77    protected void collectInitializationErrors(List<Throwable> errors) {
78        super.collectInitializationErrors(errors);
79        validateDataPointFields(errors);
80        validateDataPointMethods(errors);
81    }
82
83    private void validateDataPointFields(List<Throwable> errors) {
84        Field[] fields = getTestClass().getJavaClass().getDeclaredFields();
85
86        for (Field field : fields) {
87            if (field.getAnnotation(DataPoint.class) == null && field.getAnnotation(DataPoints.class) == null) {
88                continue;
89            }
90            if (!Modifier.isStatic(field.getModifiers())) {
91                errors.add(new Error("DataPoint field " + field.getName() + " must be static"));
92            }
93            if (!Modifier.isPublic(field.getModifiers())) {
94                errors.add(new Error("DataPoint field " + field.getName() + " must be public"));
95            }
96        }
97    }
98
99    private void validateDataPointMethods(List<Throwable> errors) {
100        Method[] methods = getTestClass().getJavaClass().getDeclaredMethods();
101
102        for (Method method : methods) {
103            if (method.getAnnotation(DataPoint.class) == null && method.getAnnotation(DataPoints.class) == null) {
104                continue;
105            }
106            if (!Modifier.isStatic(method.getModifiers())) {
107                errors.add(new Error("DataPoint method " + method.getName() + " must be static"));
108            }
109            if (!Modifier.isPublic(method.getModifiers())) {
110                errors.add(new Error("DataPoint method " + method.getName() + " must be public"));
111            }
112        }
113    }
114
115    @Override
116    protected void validateConstructor(List<Throwable> errors) {
117        validateOnlyOneConstructor(errors);
118    }
119
120    @Override
121    protected void validateTestMethods(List<Throwable> errors) {
122        for (FrameworkMethod each : computeTestMethods()) {
123            if (each.getAnnotation(Theory.class) != null) {
124                each.validatePublicVoid(false, errors);
125                each.validateNoTypeParametersOnArgs(errors);
126            } else {
127                each.validatePublicVoidNoArg(false, errors);
128            }
129
130            for (ParameterSignature signature : ParameterSignature.signatures(each.getMethod())) {
131                ParametersSuppliedBy annotation = signature.findDeepAnnotation(ParametersSuppliedBy.class);
132                if (annotation != null) {
133                    validateParameterSupplier(annotation.value(), errors);
134                }
135            }
136        }
137    }
138
139    private void validateParameterSupplier(Class<? extends ParameterSupplier> supplierClass, List<Throwable> errors) {
140        Constructor<?>[] constructors = supplierClass.getConstructors();
141
142        if (constructors.length != 1) {
143            errors.add(new Error("ParameterSupplier " + supplierClass.getName() +
144                                 " must have only one constructor (either empty or taking only a TestClass)"));
145        } else {
146            Class<?>[] paramTypes = constructors[0].getParameterTypes();
147            if (!(paramTypes.length == 0) && !paramTypes[0].equals(TestClass.class)) {
148                errors.add(new Error("ParameterSupplier " + supplierClass.getName() +
149                                     " constructor must take either nothing or a single TestClass instance"));
150            }
151        }
152    }
153
154    @Override
155    protected List<FrameworkMethod> computeTestMethods() {
156        List<FrameworkMethod> testMethods = new ArrayList<FrameworkMethod>(super.computeTestMethods());
157        List<FrameworkMethod> theoryMethods = getTestClass().getAnnotatedMethods(Theory.class);
158        testMethods.removeAll(theoryMethods);
159        testMethods.addAll(theoryMethods);
160        return testMethods;
161    }
162
163    @Override
164    public Statement methodBlock(final FrameworkMethod method) {
165        return new TheoryAnchor(method, getTestClass());
166    }
167
168    public static class TheoryAnchor extends Statement {
169        private int successes = 0;
170
171        private final FrameworkMethod testMethod;
172        private final TestClass testClass;
173
174        private List<AssumptionViolatedException> fInvalidParameters = new ArrayList<AssumptionViolatedException>();
175
176        public TheoryAnchor(FrameworkMethod testMethod, TestClass testClass) {
177            this.testMethod = testMethod;
178            this.testClass = testClass;
179        }
180
181        private TestClass getTestClass() {
182            return testClass;
183        }
184
185        @Override
186        public void evaluate() throws Throwable {
187            runWithAssignment(Assignments.allUnassigned(
188                    testMethod.getMethod(), getTestClass()));
189
190            //if this test method is not annotated with Theory, then no successes is a valid case
191            boolean hasTheoryAnnotation = testMethod.getAnnotation(Theory.class) != null;
192            if (successes == 0 && hasTheoryAnnotation) {
193                Assert
194                        .fail("Never found parameters that satisfied method assumptions.  Violated assumptions: "
195                                + fInvalidParameters);
196            }
197        }
198
199        protected void runWithAssignment(Assignments parameterAssignment)
200                throws Throwable {
201            if (!parameterAssignment.isComplete()) {
202                runWithIncompleteAssignment(parameterAssignment);
203            } else {
204                runWithCompleteAssignment(parameterAssignment);
205            }
206        }
207
208        protected void runWithIncompleteAssignment(Assignments incomplete)
209                throws Throwable {
210            for (PotentialAssignment source : incomplete
211                    .potentialsForNextUnassigned()) {
212                runWithAssignment(incomplete.assignNext(source));
213            }
214        }
215
216        protected void runWithCompleteAssignment(final Assignments complete)
217                throws Throwable {
218            new BlockJUnit4ClassRunner(getTestClass().getJavaClass()) {
219                @Override
220                protected void collectInitializationErrors(
221                        List<Throwable> errors) {
222                    // do nothing
223                }
224
225                @Override
226                public Statement methodBlock(FrameworkMethod method) {
227                    final Statement statement = super.methodBlock(method);
228                    return new Statement() {
229                        @Override
230                        public void evaluate() throws Throwable {
231                            try {
232                                statement.evaluate();
233                                handleDataPointSuccess();
234                            } catch (AssumptionViolatedException e) {
235                                handleAssumptionViolation(e);
236                            } catch (Throwable e) {
237                                reportParameterizedError(e, complete
238                                        .getArgumentStrings(nullsOk()));
239                            }
240                        }
241
242                    };
243                }
244
245                @Override
246                protected Statement methodInvoker(FrameworkMethod method, Object test) {
247                    return methodCompletesWithParameters(method, complete, test);
248                }
249
250                @Override
251                public Object createTest() throws Exception {
252                    Object[] params = complete.getConstructorArguments();
253
254                    if (!nullsOk()) {
255                        Assume.assumeNotNull(params);
256                    }
257
258                    return getTestClass().getOnlyConstructor().newInstance(params);
259                }
260            }.methodBlock(testMethod).evaluate();
261        }
262
263        private Statement methodCompletesWithParameters(
264                final FrameworkMethod method, final Assignments complete, final Object freshInstance) {
265            return new Statement() {
266                @Override
267                public void evaluate() throws Throwable {
268                    final Object[] values = complete.getMethodArguments();
269
270                    if (!nullsOk()) {
271                        Assume.assumeNotNull(values);
272                    }
273
274                    method.invokeExplosively(freshInstance, values);
275                }
276            };
277        }
278
279        protected void handleAssumptionViolation(AssumptionViolatedException e) {
280            fInvalidParameters.add(e);
281        }
282
283        protected void reportParameterizedError(Throwable e, Object... params)
284                throws Throwable {
285            if (params.length == 0) {
286                throw e;
287            }
288            throw new ParameterizedAssertionError(e, testMethod.getName(),
289                    params);
290        }
291
292        private boolean nullsOk() {
293            Theory annotation = testMethod.getMethod().getAnnotation(
294                    Theory.class);
295            if (annotation == null) {
296                return false;
297            }
298            return annotation.nullsAccepted();
299        }
300
301        protected void handleDataPointSuccess() {
302            successes++;
303        }
304    }
305}
306