Android/Testing/Unit Testing/Injecting Static Methods
You cannot redefine a static method by inheritance, but you will often want to provide a test implementation of static methods (which include the 'new' operator).
A tool like PowerMock does allow for the redefinition of static methods, but if you don't get along with it, you can still work around the issue: if you're using dependency injection, you can write trivial classes to wrap static methods and substitute them with little intrusion into your code. If you're not using real DI (due to the start-up time, for instance), the following may help you start.
If you decide to stick with this manual DI, you may find that certain methods (particularly Android ones) turn up regularly enough in different classes' tests to make it worth creating an external non-static utility class to inject. There'd be a temptation to let this grow into a grab-bag class containing all the statics you have ever needed (which probably isn't a problem beyond being a sprawling mess and potentially concealing that a class under test is doing too much), but you would want to guard against the temptation to sneak in things which aren't strictly statics "because it's a handy singleton in which to hide a bit of state" (which would be wrong).
If you're a fan of 'single responsibility', you'll see that this StaticInjection should really be broken up into separate chunks - the beginnings of proper full-scale DI (albeit static DI, rather than the dynamic style of Guice etc.).
Code under test
editAdding an inner class allows you to provide default behaviour in a production environment, but special behaviour in tests. The example shows that it's not just Android that likes declaring methods static.
/**
* Activity that does something with a Facebook token.
*/
class MyActivity extends Activity {
// Indirect static method calls, including 'new' for complex objects, to this class.
static StaticInjection STATIC_INJECTION = new StaticInjection();
StaticInjection staticInjection;
// Default constructor creates default StaticInjector
public MyActivity() {
this(STATIC_INJECTION);
}
// Non-default constructor for tests wishing to inject other implementations.
// If your tests are package-scoped, don't make this public.
MyActivity(StaticInjection staticInjection) {
this.staticInjection = staticInjection;
}
// Your unit tests can invoke this; you will need Robolectric or similar to do that on the development machine.
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
String fbToken = staticInjection.sessionStoreGetAccessToken(this);
Application app = staticInjection.getApplication(this);
DooDad doodad = staticInjection.newDooDad(this, bundle);
// use these in some way you can test
}
// Add any additional static stuff you need to stub out for your tests here.
public static class StaticInjection {
public String sessionStoreGetAccessToken(Context context) {
return com.facebook.android.SessionStore.getAccessToken(context);
}
public Application getApplication(Activity activity) {
return activity.getApplication();
}
public DooDad newDooDad(Activity activity, Bundle bundle) {
return new DooDad(activity, bundle);
}
}
}
If you find your StaticInjection class growing too much, you're probably doing too much in your Activity - try to follow the 'single responsibility' principle.
Test
editA test would either subclass and override bits of StaticInjection, or use mocking to constrain which bits of the StaticInjection were used:
@RunWith(RobolectricTestRunner.class)
public class MyActivityTest {
@Rule
public JUnitRuleMockery mockery = new ThreadSafeJUnitRuleMockery.WithImposteriser();
MyActivity activity;
@Mock Bundle bundle;
@Mock Application application;
@Mock DooDad dooDad;
@Test
public void _onCreateAccessesFacebookToken() {
MyActivity.StaticInjection injection = mockery.mock(MyActivity.StaticInjection.class);
mockery.checking(new Expectations() {{
oneOf(injection).sessionStoreGetAccessToken(activity);
will(returnValue("NotReallyAToken");
allowing(injection).getApplication(activity);
will(returnValue(application));
oneOf(injection).newDooDad(activity, bundle);
will(returnValue(dooDad));
}});
activity = new MyActivity(injection);
activity.onCreate(bundle);
// no asserts needed here: mockery will check that sessionStoreGetFacebookToken has been called once.
}
}
Misc
editThe above example depends on these trivial classes:
import org.jmock.integration.junit4.JUnitRuleMockery;
import org.jmock.lib.concurrent.Synchroniser;
import org.jmock.lib.legacy.ClassImposteriser;
/**
* Mock with extra magic stuff, use ThreadSafeJUnitRuleMockery.WithImposteriser,
* or split the classes out and rename the inner one to something sensible.
*/
public class ThreadSafeJUnitRuleMockery extends JUnitRuleMockery
{
private ThreadSafeJUnitRuleMockery()
{
setThreadingPolicy(new Synchroniser());
}
static public class WithImposteriser extends ThreadSafeJUnitRuleMockery
{
public WithImposteriser()
{
super();
setImposteriser(ClassImposteriser.INSTANCE);
}
}
static public class WithoutImposteriser extends ThreadSafeJUnitRuleMockery
{
}
}