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