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 user's filename 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.
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        @Override
077        protected void collectInitializationErrors(List<Throwable> errors) {
078            super.collectInitializationErrors(errors);
079            validateDataPointFields(errors);
080            validateDataPointMethods(errors);
081        }
082    
083        private void validateDataPointFields(List<Throwable> errors) {
084            Field[] fields = getTestClass().getJavaClass().getDeclaredFields();
085    
086            for (Field field : fields) {
087                if (field.getAnnotation(DataPoint.class) == null && field.getAnnotation(DataPoints.class) == null) {
088                    continue;
089                }
090                if (!Modifier.isStatic(field.getModifiers())) {
091                    errors.add(new Error("DataPoint field " + field.getName() + " must be static"));
092                }
093                if (!Modifier.isPublic(field.getModifiers())) {
094                    errors.add(new Error("DataPoint field " + field.getName() + " must be public"));
095                }
096            }
097        }
098    
099        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    }