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