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