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    }