RobolectricTestRunner.java revision 92bd76ae9b5439412a2bee3d3bad45319090015d
1package com.xtremelabs.robolectric; 2 3import android.app.Application; 4import android.net.Uri__FromAndroid; 5import com.xtremelabs.robolectric.bytecode.ClassHandler; 6import com.xtremelabs.robolectric.bytecode.RobolectricClassLoader; 7import com.xtremelabs.robolectric.bytecode.ShadowWrangler; 8import com.xtremelabs.robolectric.internal.RealObject; 9import com.xtremelabs.robolectric.internal.RobolectricTestRunnerInterface; 10import com.xtremelabs.robolectric.res.ResourceLoader; 11import com.xtremelabs.robolectric.shadows.ShadowApplication; 12import com.xtremelabs.robolectric.shadows.ShadowLog; 13import com.xtremelabs.robolectric.util.DatabaseConfig; 14import com.xtremelabs.robolectric.util.DatabaseConfig.DatabaseMap; 15import com.xtremelabs.robolectric.util.DatabaseConfig.UsingDatabaseMap; 16import com.xtremelabs.robolectric.util.SQLiteMap; 17import javassist.Loader; 18import org.junit.runners.BlockJUnit4ClassRunner; 19import org.junit.runners.model.FrameworkMethod; 20import org.junit.runners.model.InitializationError; 21import org.junit.runners.model.Statement; 22import org.w3c.dom.Document; 23import org.xml.sax.SAXException; 24 25import javax.xml.parsers.DocumentBuilder; 26import javax.xml.parsers.DocumentBuilderFactory; 27import javax.xml.parsers.ParserConfigurationException; 28import java.io.*; 29import java.lang.annotation.Annotation; 30import java.lang.reflect.Constructor; 31import java.lang.reflect.Field; 32import java.lang.reflect.Method; 33import java.lang.reflect.Modifier; 34import java.util.HashMap; 35import java.util.Map; 36 37/** 38 * Installs a {@link RobolectricClassLoader} and {@link com.xtremelabs.robolectric.res.ResourceLoader} in order to 39 * provide a simulation of the Android runtime environment. 40 */ 41public class RobolectricTestRunner extends BlockJUnit4ClassRunner implements RobolectricTestRunnerInterface { 42 43 /** Instrument detector. We use it to check whether the current instance is instrumented. */ 44 private static InstrumentDetector instrumentDetector = InstrumentDetector.DEFAULT; 45 46 private static RobolectricClassLoader defaultLoader; 47 private static Map<RobolectricConfig, ResourceLoader> resourceLoaderForRootAndDirectory = new HashMap<RobolectricConfig, ResourceLoader>(); 48 49 // fields in the RobolectricTestRunner in the original ClassLoader 50 private RobolectricClassLoader classLoader; 51 private ClassHandler classHandler; 52 private RobolectricTestRunnerInterface delegate; 53 private DatabaseMap databaseMap; 54 55 // fields in the RobolectricTestRunner in the instrumented ClassLoader 56 protected RobolectricConfig robolectricConfig; 57 58 private static RobolectricClassLoader getDefaultLoader() { 59 if (defaultLoader == null) { 60 defaultLoader = new RobolectricClassLoader(ShadowWrangler.getInstance()); 61 } 62 return defaultLoader; 63 } 64 65 public static void setInstrumentDetector(final InstrumentDetector detector) { 66 instrumentDetector = detector; 67 } 68 69 public static void setDefaultLoader(Loader robolectricClassLoader) { 70 //used by the RoboSpecs project to allow for mixed scala\java tests to be run with Maven Surefire (see the RoboSpecs project on github) 71 if (defaultLoader == null) { 72 defaultLoader = (RobolectricClassLoader)robolectricClassLoader; 73 } else throw new RuntimeException("You may not set the default robolectricClassLoader unless it is null!"); 74 } 75 76 /** 77 * Call this if you would like Robolectric to rewrite additional classes and turn them 78 * into "do nothing" classes which proxy all method calls to shadow classes, just like it does 79 * with the android classes by default. 80 * 81 * @param classOrPackageToBeInstrumented fully-qualified class or package name 82 */ 83 protected static void addClassOrPackageToInstrument(String classOrPackageToBeInstrumented) { 84 if (!isInstrumented()) { 85 defaultLoader.addCustomShadowClass(classOrPackageToBeInstrumented); 86 } 87 } 88 89 /** 90 * Creates a runner to run {@code testClass}. Looks in your working directory for your AndroidManifest.xml file 91 * and res directory. 92 * 93 * @param testClass the test class to be run 94 * @throws InitializationError if junit says so 95 */ 96 public RobolectricTestRunner(final Class<?> testClass) throws InitializationError { 97 this(testClass, new RobolectricConfig(new File("."))); 98 } 99 100 /** 101 * Call this constructor in subclasses in order to specify non-default configuration (e.g. location of the 102 * AndroidManifest.xml file and resource directory). 103 * 104 * @param testClass the test class to be run 105 * @param robolectricConfig the configuration data 106 * @throws InitializationError if junit says so 107 */ 108 protected RobolectricTestRunner(final Class<?> testClass, final RobolectricConfig robolectricConfig) 109 throws InitializationError { 110 this(testClass, 111 isInstrumented() ? null : ShadowWrangler.getInstance(), 112 isInstrumented() ? null : getDefaultLoader(), 113 robolectricConfig, new SQLiteMap()); 114 } 115 116 /** 117 * Call this constructor in subclasses in order to specify non-default configuration (e.g. location of the 118 * AndroidManifest.xml file, resource directory, and DatabaseMap). 119 * 120 * @param testClass the test class to be run 121 * @param robolectricConfig the configuration data 122 * @param databaseMap the database mapping 123 * @throws InitializationError if junit says so 124 */ 125 protected RobolectricTestRunner(Class<?> testClass, RobolectricConfig robolectricConfig, DatabaseMap databaseMap) 126 throws InitializationError { 127 this(testClass, 128 isInstrumented() ? null : ShadowWrangler.getInstance(), 129 isInstrumented() ? null : getDefaultLoader(), 130 robolectricConfig, databaseMap); 131 } 132 133 /** 134 * Call this constructor in subclasses in order to specify the project root directory. 135 * 136 * @param testClass the test class to be run 137 * @param androidProjectRoot the directory containing your AndroidManifest.xml file and res dir 138 * @throws InitializationError if the test class is malformed 139 */ 140 public RobolectricTestRunner(final Class<?> testClass, final File androidProjectRoot) throws InitializationError { 141 this(testClass, new RobolectricConfig(androidProjectRoot)); 142 } 143 144 /** 145 * Call this constructor in subclasses in order to specify the project root directory. 146 * 147 * @param testClass the test class to be run 148 * @param androidProjectRoot the directory containing your AndroidManifest.xml file and res dir 149 * @throws InitializationError if junit says so 150 * @deprecated Use {@link #RobolectricTestRunner(Class, File)} instead. 151 */ 152 @Deprecated 153 public RobolectricTestRunner(final Class<?> testClass, final String androidProjectRoot) throws InitializationError { 154 this(testClass, new RobolectricConfig(new File(androidProjectRoot))); 155 } 156 157 /** 158 * Call this constructor in subclasses in order to specify the location of the AndroidManifest.xml file and the 159 * resource directory. The #androidManifestPath is used to locate the AndroidManifest.xml file which, in turn, 160 * contains package name for the {@code R} class which contains the identifiers for all of the resources. The 161 * resource directory is where the resource loader will look for resources to load. 162 * 163 * @param testClass the test class to be run 164 * @param androidManifestPath the AndroidManifest.xml file 165 * @param resourceDirectory the directory containing the project's resources 166 * @throws InitializationError if junit says so 167 */ 168 protected RobolectricTestRunner(final Class<?> testClass, final File androidManifestPath, final File resourceDirectory) 169 throws InitializationError { 170 this(testClass, new RobolectricConfig(androidManifestPath, resourceDirectory)); 171 } 172 173 /** 174 * Call this constructor in subclasses in order to specify the location of the AndroidManifest.xml file and the 175 * resource directory. The #androidManifestPath is used to locate the AndroidManifest.xml file which, in turn, 176 * contains package name for the {@code R} class which contains the identifiers for all of the resources. The 177 * resource directory is where the resource loader will look for resources to load. 178 * 179 * @param testClass the test class to be run 180 * @param androidManifestPath the relative path to the AndroidManifest.xml file 181 * @param resourceDirectory the relative path to the directory containing the project's resources 182 * @throws InitializationError if junit says so 183 * @deprecated Use {@link #RobolectricTestRunner(Class, File, File)} instead. 184 */ 185 @Deprecated 186 protected RobolectricTestRunner(final Class<?> testClass, final String androidManifestPath, final String resourceDirectory) 187 throws InitializationError { 188 this(testClass, new RobolectricConfig(new File(androidManifestPath), new File(resourceDirectory))); 189 } 190 191 protected RobolectricTestRunner(Class<?> testClass, ClassHandler classHandler, RobolectricClassLoader classLoader, RobolectricConfig robolectricConfig) throws InitializationError { 192 this(testClass, classHandler, classLoader, robolectricConfig, new SQLiteMap()); 193 } 194 195 196 /** 197 * This is not the constructor you are looking for... probably. This constructor creates a bridge between the test 198 * runner called by JUnit and a second instance of the test runner that is loaded via the instrumenting class 199 * loader. This instrumented instance of the test runner, along with the instrumented instance of the actual test, 200 * provides access to Robolectric's features and the un-instrumented instance of the test runner delegates most of 201 * the interesting test runner behavior to it. Providing your own class handler and class loader here in order to 202 * get different functionality is a difficult and dangerous project. If you need to customize the project root and 203 * resource directory, use {@link #RobolectricTestRunner(Class, String, String)}. For other extensions, consider 204 * creating a subclass and overriding the documented methods of this class. 205 * 206 * @param testClass the test class to be run 207 * @param classHandler the {@link ClassHandler} to use to in shadow delegation 208 * @param classLoader the {@link RobolectricClassLoader} 209 * @param robolectricConfig the configuration 210 * @throws InitializationError if junit says so 211 */ 212 protected RobolectricTestRunner(final Class<?> testClass, final ClassHandler classHandler, final RobolectricClassLoader classLoader, final RobolectricConfig robolectricConfig, final DatabaseMap map) throws InitializationError { 213 super(isInstrumented() ? testClass : classLoader.bootstrap(testClass)); 214 215 if (!isInstrumented()) { 216 this.classHandler = classHandler; 217 this.classLoader = classLoader; 218 this.robolectricConfig = robolectricConfig; 219 this.databaseMap = setupDatabaseMap(testClass, map); 220 221 Thread.currentThread().setContextClassLoader(classLoader); 222 223 delegateLoadingOf(Uri__FromAndroid.class.getName()); 224 delegateLoadingOf(RobolectricTestRunnerInterface.class.getName()); 225 delegateLoadingOf(RealObject.class.getName()); 226 delegateLoadingOf(ShadowWrangler.class.getName()); 227 delegateLoadingOf(RobolectricConfig.class.getName()); 228 delegateLoadingOf(DatabaseMap.class.getName()); 229 delegateLoadingOf(android.R.class.getName()); 230 231 Class<?> delegateClass = classLoader.bootstrap(this.getClass()); 232 try { 233 Constructor<?> constructorForDelegate = delegateClass.getConstructor(Class.class); 234 this.delegate = (RobolectricTestRunnerInterface) constructorForDelegate.newInstance(classLoader.bootstrap(testClass)); 235 this.delegate.setRobolectricConfig(robolectricConfig); 236 this.delegate.setDatabaseMap(databaseMap); 237 } catch (Exception e) { 238 throw new RuntimeException(e); 239 } 240 } 241 } 242 243 protected static boolean isInstrumented() { 244 return instrumentDetector.isInstrumented(); 245 } 246 247 /** 248 * Only used when creating the delegate instance within the instrumented ClassLoader. 249 * <p/> 250 * This is not the constructor you are looking for. 251 */ 252 @SuppressWarnings({"UnusedDeclaration", "JavaDoc"}) 253 protected RobolectricTestRunner(final Class<?> testClass, final ClassHandler classHandler, final RobolectricConfig robolectricConfig) throws InitializationError { 254 super(testClass); 255 this.classHandler = classHandler; 256 this.robolectricConfig = robolectricConfig; 257 } 258 259 public static void setStaticValue(Class<?> clazz, String fieldName, Object value) { 260 try { 261 Field field = clazz.getField(fieldName); 262 field.setAccessible(true); 263 264 Field modifiersField = Field.class.getDeclaredField("modifiers"); 265 modifiersField.setAccessible(true); 266 modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); 267 268 field.set(null, value); 269 } catch (Exception e) { 270 throw new RuntimeException(e); 271 } 272 } 273 274 protected void delegateLoadingOf(final String className) { 275 classLoader.delegateLoadingOf(className); 276 } 277 278 @Override protected Statement methodBlock(final FrameworkMethod method) { 279 setupI18nStrictState(method.getMethod(), robolectricConfig); 280 281 if (classHandler != null) { 282 classHandler.configure(robolectricConfig); 283 classHandler.beforeTest(); 284 } 285 delegate.internalBeforeTest(method.getMethod()); 286 287 final Statement statement = super.methodBlock(method); 288 return new Statement() { 289 @Override public void evaluate() throws Throwable { 290 // todo: this try/finally probably isn't right -- should mimic RunAfters? [xw] 291 try { 292 statement.evaluate(); 293 } finally { 294 delegate.internalAfterTest(method.getMethod()); 295 if (classHandler != null) { 296 classHandler.afterTest(); 297 } 298 } 299 } 300 }; 301 } 302 303 /* 304 * Called before each test method is run. Sets up the simulation of the Android runtime environment. 305 */ 306 @Override public void internalBeforeTest(final Method method) { 307 setupApplicationState(robolectricConfig); 308 309 beforeTest(method); 310 } 311 312 @Override public void internalAfterTest(final Method method) { 313 afterTest(method); 314 } 315 316 @Override public void setRobolectricConfig(final RobolectricConfig robolectricConfig) { 317 this.robolectricConfig = robolectricConfig; 318 } 319 320 /** 321 * Called before each test method is run. 322 * 323 * @param method the test method about to be run 324 */ 325 public void beforeTest(final Method method) { 326 } 327 328 /** 329 * Called after each test method is run. 330 * 331 * @param method the test method that just ran. 332 */ 333 public void afterTest(final Method method) { 334 } 335 336 /** 337 * You probably don't want to override this method. Override #prepareTest(Object) instead. 338 * 339 * @see BlockJUnit4ClassRunner#createTest() 340 */ 341 @Override 342 public Object createTest() throws Exception { 343 if (delegate != null) { 344 return delegate.createTest(); 345 } else { 346 Object test = super.createTest(); 347 prepareTest(test); 348 return test; 349 } 350 } 351 352 public void prepareTest(final Object test) { 353 } 354 355 public void setupApplicationState(final RobolectricConfig robolectricConfig) { 356 setupLogging(); 357 ResourceLoader resourceLoader = createResourceLoader(robolectricConfig); 358 359 Robolectric.bindDefaultShadowClasses(); 360 bindShadowClasses(); 361 362 resourceLoader.setLayoutQualifierSearchPath(); 363 Robolectric.resetStaticState(); 364 resetStaticState(); 365 366 DatabaseConfig.setDatabaseMap(this.databaseMap);//Set static DatabaseMap in DBConfig 367 368 Robolectric.application = ShadowApplication.bind(createApplication(), resourceLoader); 369 } 370 371 372 373 /** 374 * Override this method to bind your own shadow classes 375 */ 376 protected void bindShadowClasses() { 377 } 378 379 /** 380 * Override this method to reset the state of static members before each test. 381 */ 382 protected void resetStaticState() { 383 } 384 385 /** 386 * Sets Robolectric config to determine if Robolectric should blacklist API calls that are not 387 * I18N/L10N-safe. 388 * <p/> 389 * I18n-strict mode affects suitably annotated shadow methods. Robolectric will throw exceptions 390 * if these methods are invoked by application code. Additionally, Robolectric's ResourceLoader 391 * will throw exceptions if layout resources use bare string literals instead of string resource IDs. 392 * <p/> 393 * To enable or disable i18n-strict mode for specific test cases, annotate them with 394 * {@link com.xtremelabs.robolectric.annotation.EnableStrictI18n} or 395 * {@link com.xtremelabs.robolectric.annotation.DisableStrictI18n}. 396 * <p/> 397 * 398 * By default, I18n-strict mode is disabled. 399 * 400 * @param method 401 * @param robolectricConfig 402 */ 403 private void setupI18nStrictState(Method method, RobolectricConfig robolectricConfig) { 404 // Global 405 boolean strictI18n = globalI18nStrictEnabled(); 406 407 // Test case class 408 Annotation[] annos = method.getDeclaringClass().getAnnotations(); 409 strictI18n = lookForI18nAnnotations(strictI18n, annos); 410 411 // Test case methods 412 annos = method.getAnnotations(); 413 strictI18n = lookForI18nAnnotations(strictI18n, annos); 414 415 robolectricConfig.setStrictI18n(strictI18n); 416 } 417 418 /** 419 * Default implementation of global switch for i18n-strict mode. 420 * To enable i18n-strict mode globally, set the system property 421 * "robolectric.strictI18n" to true. This can be done via java 422 * system properties in either Ant or Maven. 423 * <p/> 424 * Subclasses can override this method and establish their own policy 425 * for enabling i18n-strict mode. 426 * 427 * @return 428 */ 429 protected boolean globalI18nStrictEnabled() { 430 return Boolean.valueOf(System.getProperty("robolectric.strictI18n")); 431 } 432 433 /** 434 * As test methods are loaded by the delegate's class loader, the normal 435 * method#isAnnotationPresent test fails. Look at string versions of the 436 * annotation names to test for their presence. 437 * 438 * @param strictI18n 439 * @param annos 440 * @return 441 */ 442 private boolean lookForI18nAnnotations(boolean strictI18n, Annotation[] annos) { 443 for ( int i = 0; i < annos.length; i++ ) { 444 String name = annos[i].annotationType().getName(); 445 if (name.equals("com.xtremelabs.robolectric.annotation.EnableStrictI18n")) { 446 strictI18n = true; 447 break; 448 } 449 if (name.equals("com.xtremelabs.robolectric.annotation.DisableStrictI18n")) { 450 strictI18n = false; 451 break; 452 } 453 } 454 return strictI18n; 455 } 456 457 private void setupLogging() { 458 String logging = System.getProperty("robolectric.logging"); 459 if (logging != null && ShadowLog.stream == null) { 460 PrintStream stream = null; 461 if ("stdout".equalsIgnoreCase(logging)) { 462 stream = System.out; 463 } else if ("stderr".equalsIgnoreCase(logging)) { 464 stream = System.err; 465 } else { 466 try { 467 final PrintStream file = new PrintStream(new FileOutputStream(logging)); 468 stream = file; 469 Runtime.getRuntime().addShutdownHook(new Thread() { 470 @Override public void run() { 471 try { file.close(); } catch (Exception ignored) { } 472 } 473 }); 474 } catch (IOException e) { 475 e.printStackTrace(); 476 } 477 } 478 ShadowLog.stream = stream; 479 } 480 } 481 482 /** 483 * Override this method if you want to provide your own implementation of Application. 484 * <p/> 485 * This method attempts to instantiate an application instance as specified by the AndroidManifest.xml. 486 * 487 * @return An instance of the Application class specified by the ApplicationManifest.xml or an instance of 488 * Application if not specified. 489 */ 490 protected Application createApplication() { 491 return new ApplicationResolver(robolectricConfig).resolveApplication(); 492 } 493 494 private ResourceLoader createResourceLoader(final RobolectricConfig robolectricConfig) { 495 ResourceLoader resourceLoader = resourceLoaderForRootAndDirectory.get(robolectricConfig); 496 if (resourceLoader == null) { 497 try { 498 robolectricConfig.validate(); 499 500 String rClassName = robolectricConfig.getRClassName(); 501 Class rClass = Class.forName(rClassName); 502 resourceLoader = new ResourceLoader(robolectricConfig.getRealSdkVersion(), rClass, robolectricConfig.getResourceDirectory(), robolectricConfig.getAssetsDirectory()); 503 resourceLoaderForRootAndDirectory.put(robolectricConfig, resourceLoader); 504 } catch (Exception e) { 505 throw new RuntimeException(e); 506 } 507 } 508 509 resourceLoader.setStrictI18n(robolectricConfig.getStrictI18n()); 510 return resourceLoader; 511 } 512 513 private String findResourcePackageName(final File projectManifestFile) throws ParserConfigurationException, IOException, SAXException { 514 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); 515 DocumentBuilder db = dbf.newDocumentBuilder(); 516 Document doc = db.parse(projectManifestFile); 517 518 String projectPackage = doc.getElementsByTagName("manifest").item(0).getAttributes().getNamedItem("package").getTextContent(); 519 520 return projectPackage + ".R"; 521 } 522 523 /* 524 * Specifies what database to use for testing (ex: H2 or Sqlite), 525 * this will load H2 by default, the SQLite TestRunner version will override this. 526 */ 527 protected DatabaseMap setupDatabaseMap(Class<?> testClass, DatabaseMap map) { 528 DatabaseMap dbMap = map; 529 530 if (testClass.isAnnotationPresent(UsingDatabaseMap.class)) { 531 UsingDatabaseMap usingMap = testClass.getAnnotation(UsingDatabaseMap.class); 532 if(usingMap.value()!=null){ 533 dbMap = Robolectric.newInstanceOf(usingMap.value()); 534 } else { 535 if (dbMap==null) 536 throw new RuntimeException("UsingDatabaseMap annotation value must provide a class implementing DatabaseMap"); 537 } 538 } 539 return dbMap; 540 } 541 542 public DatabaseMap getDatabaseMap() { 543 return databaseMap; 544 } 545 546 public void setDatabaseMap(DatabaseMap databaseMap) { 547 this.databaseMap = databaseMap; 548 } 549 550 /** 551 * Detects whether current instance is already instrumented. 552 */ 553 public interface InstrumentDetector { 554 555 /** Default detector. */ 556 InstrumentDetector DEFAULT = new InstrumentDetector() { 557 @Override 558 public boolean isInstrumented() { 559 return RobolectricTestRunner.class.getClassLoader().getClass().getName().contains(RobolectricClassLoader.class.getName()); 560 } 561 }; 562 563 /** 564 * @return true if current instance is already instrumented 565 */ 566 boolean isInstrumented(); 567 568 } 569 570} 571