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 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 }