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>@Theory</b>. To create a data point you create a public
026 * field in your test class and mark it with <b>@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 * @RunWith(<b>Theories.class</b>)
041 * public class UserTest {
042 * <b>@DataPoint</b>
043 * public static String GOOD_USERNAME = "optimus";
044 * <b>@DataPoint</b>
045 * public static String USERNAME_WITH_SLASH = "optimus/prime";
046 *
047 * <b>@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 }