001package org.junit.experimental.theories;
002
003import java.lang.reflect.Constructor;
004import java.lang.reflect.Field;
005import java.lang.reflect.Method;
006import java.lang.reflect.Modifier;
007import java.util.ArrayList;
008import java.util.List;
009
010import org.junit.Assert;
011import org.junit.Assume;
012import org.junit.experimental.theories.internal.Assignments;
013import org.junit.experimental.theories.internal.ParameterizedAssertionError;
014import org.junit.internal.AssumptionViolatedException;
015import org.junit.runners.BlockJUnit4ClassRunner;
016import org.junit.runners.model.FrameworkMethod;
017import org.junit.runners.model.InitializationError;
018import org.junit.runners.model.Statement;
019import 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 */
071public 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}