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 }