How does exactly custom Shadow objects work in Robolectric?
If I write a custom Shadow for my Activity, and registering it with RobolectricTestRunner, wi开发者_如何学编程ll the framework intercept the Activity with my custom Shadow whenever it's started?
Thanks.
The short answer is no.
Robolectric is selective about what classes it intercepts and instruments. At the time of this writing, the only classes that will be instrumented must have a fully qualified classname match one of these selectors:
android.*
com.google.android.maps.*
org.apache.http.impl.client.DefaultRequestDirector
The whole reason for Robolectric's existence is that the classes provided in the Android SDK jar throw exceptions when invoked in a JVM (i.e. not on an emulator or device). Your application's Activity has source that is not 'hostile' (it probably does not throw exceptions when the methods or constructors are invoked). Robolectric's intended purpose is to allow you to put your application's code under test, which would otherwise not be possible due to the way the SDK is written. Some of the other reasons why Robolectric was created were:
- The SDK does not always have methods that would allow you to query the state of the Android objects manipulated by your application's code. Shadows can be written to provide access to this state.
- Many of the classes and methods in the Android SDK are final and/or private or protected, making it difficult to create the dependencies needed by your application code that would otherwise be available to your application code.
The code could clearly be changed to shadow any class. There has been talk in the past about extracting the shadowing features into a standalone library, to assist writing tests using some other test-hostile api.
Why do you want to shadow your Activity?
This has significantly changed with Robolectric 2. You can specify custom shadows in the configuration instead of writing your own TestRunner.
For example:
@Config(shadows = {ShadowAudioManager.class, ShadowContextWrapper.class})
Yes, if you subclass the RobolectricTestRunner, add a custom package to the constructor and load your Shadow classes in the bindShadowClasses method. No need to use the android.* package trick.
(Note: this is with robolectric-1.1)
There are a number of hooks provided in the RobolectricTestRunner#setupApplicationState that you can override.
Here's my implementation of the RobolectricTestRunner.
import org.junit.runners.model.InitializationError;
import com.android.testFramework.shadows.ShadowLoggerConfig;
import com.xtremelabs.robolectric.Robolectric;
import com.xtremelabs.robolectric.RobolectricTestRunner;
public class RoboRunner extends RobolectricTestRunner {
public RoboRunner(Class<?> clazz) throws InitializationError {
super(clazz);
addClassOrPackageToInstrument("package.you're.creating.shadows.of");
}
@Override
protected void bindShadowClasses() {
super.bindShadowClasses(); // as you can see below, you really don't need this
Robolectric.bindShadowClass(ShadowClass.class);
}
}
More methods you can subclass (from RobolectricTestRunner.class)
/**
* Override this method to bind your own shadow classes
*/
protected void bindShadowClasses() {
}
/**
* Override this method to reset the state of static members before each test.
*/
protected void resetStaticState() {
}
/**
* Override this method if you want to provide your own implementation of Application.
* <p/>
* This method attempts to instantiate an application instance as specified by the AndroidManifest.xml.
*
* @return An instance of the Application class specified by the ApplicationManifest.xml or an instance of
* Application if not specified.
*/
protected Application createApplication() {
return new ApplicationResolver(robolectricConfig).resolveApplication();
}
Here's where they're called in the Robolectric TestRunner:
public void setupApplicationState(final RobolectricConfig robolectricConfig) {
setupLogging();
ResourceLoader resourceLoader = createResourceLoader(robolectricConfig);
Robolectric.bindDefaultShadowClasses();
bindShadowClasses();
resourceLoader.setLayoutQualifierSearchPath();
Robolectric.resetStaticState();
resetStaticState();
DatabaseConfig.setDatabaseMap(this.databaseMap);//Set static DatabaseMap in DBConfig
Robolectric.application = ShadowApplication.bind(createApplication(), resourceLoader);
}
As an update, I have been able to create shadows of my own classes, as long as am careful to bind the shadow class before any possible loader acts on that class. So, per the instructions, in the RoboRunner I did:
@Override protected void bindShadowClasses() {
Robolectric.bindShadowClass(ShadowLog.class);
Robolectric.bindShadowClass(ShadowFlashPlayerFinder.class);
}
Did I mention that I'm cheating a bit? The original answer above is (of course) correct. So I use this for my real class:
package android.niftyco;
public class FlashPlayerFinder {
.. .
And my mock (shadow) is in back in my test package, as one might expect:
package com.niftyco.android.test;
@Implements(FlashPlayerFinder.class)
public class ShadowFlashPlayerFinder {
@RealObject private FlashPlayerFinder realFPF;
public void __constructor(Context c) {
//note the construction
}
@Implementation
public boolean isFlashInstalled() {
System.out.print("Let's pretend that Flash is installed\n");
return(true);
}
}
Might be late, but from here: org.robolectric.bytecode.Setup, you might find further detail about what classes are instrumented.
public boolean shouldInstrument(ClassInfo classInfo) {
if (classInfo.isInterface() || classInfo.isAnnotation() || classInfo.hasAnnotation(DoNotInstrument.class)) {
return false;
}
// allow explicit control with @Instrument, mostly for tests
return classInfo.hasAnnotation(Instrument.class) || isFromAndroidSdk(classInfo);
}
public boolean isFromAndroidSdk(ClassInfo classInfo) {
String className = classInfo.getName();
return className.startsWith("android.")
|| className.startsWith("libcore.")
|| className.startsWith("dalvik.")
|| className.startsWith("com.android.internal.")
|| className.startsWith("com.google.android.maps.")
|| className.startsWith("com.google.android.gms.")
|| className.startsWith("dalvik.system.")
|| className.startsWith("org.apache.http.impl.client.DefaultRequestDirector");
}
精彩评论