Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to prevent ActivityUnitTestCase from calling Application.onCreate?

I must be missing something here. The JavaDoc of ActivityUnitTestCase suggest that this test case tests an Activity in isolation from the system:

This class provides isolated testing of a single activity. The activity under test will be created with minimal connection to the system infrastructure, and you can inject mocked or wrappered versions of many of Activity's dependencies.

I was assuming that includes not actually starting the application. Moreover, it exposes a setApplication helper that one can presumably use to inject a mock application.

However, any ActivityUnitTestCase I start launches the (actual) application and calls its onCreate method. More precisely, the InstrumentationTestRunner seems to be doing that, and doing so even before I get a chance to setApplication in my test's setUp method! I didn't even notice that for quite a while, since it seems to happen at a point during test suite launch where not even Eclipse breakpoints are reached, but writing to the logs in onCreate reveals that it's actually called.

This is completely beyond me. Why would I want to use a mock app object when Android's test runner instantiates and executes the actual application anyway? This is even more problematic considering that the instrumentation runner runs in its own thread, and spawns the main application thread when doing so. This means there is a race condition between the test being executed and Application.onCreate being called. If you do anything in there that could affect your tests, e.g. writing to a shared preference file, then you're completely screwed, since your tests will randomly fail.

Am I missing something or is this simply a gross oversight in the test framework?

UPDATE This seems to affect ApplicationTestCase as well. Before my test case is even started, I can reach a breakpoint in my application class' onCreate. We start a fire-and-forget AsyncTask in there, which will randomly fail because I get no chance to mock it out (remember, that's before setUp is called on my test case). Here is the stack trace I see during this obscure invocation of onCreate:

Thread [<1> main] (Suspended (breakpoint at line 86 in QypeRadar))  
QypeRadar.onCreate() line: 86   
InstrumentationTestRunner(Instrumentation).callApplicationOnCreate(Application) line: 969   
ActivityThread.handleBindApplication(ActivityThread$AppBindData) line: 4244 
ActivityThread.access$3000(ActivityThread, ActivityThread$AppBindData) line: 125    
ActivityThread$H.handleMessage(Message) line: 2071  
ActivityThread$H(Handler).dispatchMessage(Message) line: 99 
Looper.loop() line: 123 
ActivityThread.main(String[]) line: 4627    
Method.invokeNative(Object, Object[], Class, Class[], Class, int, boolean) line: not available [native method]  
Method.invoke(Object, Object...) line: 521  
ZygoteInit$MethodAndArgsCaller.run() line: 868  
ZygoteInit.main(String[]) line: 626 
NativeStart.main(String[]) line: not available [native method]  

Why does the test runner callApplicationOnCreate even though the docs clearly state:

The test case will not call onCreate() until your test calls createApplication(). This gives you a chance to set up or adjust any additional framework or test logic before onCreate().

That's a flat out lie--it doesn't give me the chance!

like image 827
Matthias Avatar asked Feb 11 '11 13:02

Matthias


2 Answers

Roboguice had the same issue. Check it here.

like image 165
Macarse Avatar answered Sep 21 '22 12:09

Macarse


I'm doing tests with dagger, so probably this is your case too, since you probably want to just inject and do not call whatever is in Application.onCreate, so this one is works fine for me (api17+):

private Context mContext;
private Application mApplication;

@Override
protected void setUp() throws Exception {
    super.setUp();

    mContext = new ContextWrapper(getInstrumentation().getTargetContext()) {
        @Override
        public Context getApplicationContext() {
            return mApplication;
        }
    };
    mApplication = new MyAppMock();
    mApplication.attachBaseContext(mContext); 

    setApplication(app);
}

public void testActivityCreated() {
    Intent intent = AboutActivity.createIntent(mContext);
    setActivityContext(mContext);
    startActivity(intent, null, null);
    assertNotNull(getActivity());
}

For < api16 you need to use reflection and call Application.attach(context) instead Application.attachBaseContext() to set Application.mLoadedApk, otherwise it will crash.

I have put everything together and made demo app that shows how to test with dagger: https://github.com/vovkab/dagger-unit-test

It also shows how to mock your application, works for any android version.

like image 41
vovkab Avatar answered Sep 18 '22 12:09

vovkab