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 }