1package org.junit.experimental.theories; 2 3import java.lang.reflect.Constructor; 4import java.lang.reflect.Field; 5import java.lang.reflect.Method; 6import java.lang.reflect.Modifier; 7import java.util.ArrayList; 8import java.util.List; 9 10import org.junit.Assert; 11import org.junit.Assume; 12import org.junit.experimental.theories.internal.Assignments; 13import org.junit.experimental.theories.internal.ParameterizedAssertionError; 14import org.junit.internal.AssumptionViolatedException; 15import org.junit.runners.BlockJUnit4ClassRunner; 16import org.junit.runners.model.FrameworkMethod; 17import org.junit.runners.model.InitializationError; 18import org.junit.runners.model.Statement; 19import org.junit.runners.model.TestClass; 20 21/** 22 * The Theories runner allows to test a certain functionality against a subset of an infinite set of data points. 23 * <p> 24 * A Theory is a piece of functionality (a method) that is executed against several data inputs called data points. 25 * To make a test method a theory you mark it with <b>@Theory</b>. To create a data point you create a public 26 * field in your test class and mark it with <b>@DataPoint</b>. The Theories runner then executes your test 27 * method as many times as the number of data points declared, providing a different data point as 28 * the input argument on each invocation. 29 * </p> 30 * <p> 31 * A Theory differs from standard test method in that it captures some aspect of the intended behavior in possibly 32 * infinite numbers of scenarios which corresponds to the number of data points declared. Using assumptions and 33 * assertions properly together with covering multiple scenarios with different data points can make your tests more 34 * flexible and bring them closer to scientific theories (hence the name). 35 * </p> 36 * <p> 37 * For example: 38 * <pre> 39 * 40 * @RunWith(<b>Theories.class</b>) 41 * public class UserTest { 42 * <b>@DataPoint</b> 43 * public static String GOOD_USERNAME = "optimus"; 44 * <b>@DataPoint</b> 45 * public static String USERNAME_WITH_SLASH = "optimus/prime"; 46 * 47 * <b>@Theory</b> 48 * public void filenameIncludesUsername(String username) { 49 * assumeThat(username, not(containsString("/"))); 50 * assertThat(new User(username).configFileName(), containsString(username)); 51 * } 52 * } 53 * </pre> 54 * This makes it clear that the user's filename should be included in the config file name, 55 * only if it doesn't contain a slash. Another test or theory might define what happens when a username does contain 56 * a slash. <code>UserTest</code> will attempt to run <code>filenameIncludesUsername</code> on every compatible data 57 * point defined in the class. If any of the assumptions fail, the data point is silently ignored. If all of the 58 * assumptions pass, but an assertion fails, the test fails. 59 * <p> 60 * Defining general statements as theories allows data point reuse across a bunch of functionality tests and also 61 * allows automated tools to search for new, unexpected data points that expose bugs. 62 * </p> 63 * <p> 64 * The support for Theories has been absorbed from the Popper project, and more complete documentation can be found 65 * from that projects archived documentation. 66 * </p> 67 * 68 * @see <a href="http://web.archive.org/web/20071012143326/popper.tigris.org/tutorial.html">Archived Popper project documentation</a> 69 * @see <a href="http://web.archive.org/web/20110608210825/http://shareandenjoy.saff.net/tdd-specifications.pdf">Paper on Theories</a> 70 */ 71public class Theories extends BlockJUnit4ClassRunner { 72 public Theories(Class<?> klass) throws InitializationError { 73 super(klass); 74 } 75 76 @Override 77 protected void collectInitializationErrors(List<Throwable> errors) { 78 super.collectInitializationErrors(errors); 79 validateDataPointFields(errors); 80 validateDataPointMethods(errors); 81 } 82 83 private void validateDataPointFields(List<Throwable> errors) { 84 Field[] fields = getTestClass().getJavaClass().getDeclaredFields(); 85 86 for (Field field : fields) { 87 if (field.getAnnotation(DataPoint.class) == null && field.getAnnotation(DataPoints.class) == null) { 88 continue; 89 } 90 if (!Modifier.isStatic(field.getModifiers())) { 91 errors.add(new Error("DataPoint field " + field.getName() + " must be static")); 92 } 93 if (!Modifier.isPublic(field.getModifiers())) { 94 errors.add(new Error("DataPoint field " + field.getName() + " must be public")); 95 } 96 } 97 } 98 99 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} 306