javaandroidautomated-testsinstrumentationandroid-junit

Test recreating Android Activity using instrumentation and JUnit4


I want to write test for recreating activity. Performing rotation is optional.

I want the test to be written in up-to-date version of testing framework "blessed" by Google. I am new to writing tests, so I want to learn basic, main-stream, well supported tools. Any 3rd party testing frameworks will be fine when I grasp basics. And since I want to test very basic, frequently occuring scenario, basic tool should suffice, right?

Minimal test code:

public class MainActivity extends AppCompatActivity {

    static int creationCounter = 0;
    Integer instanceId;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        ++creationCounter;
        instanceId = new Integer(creationCounter);
        Log.d("TEST", "creating activity " + this + ", has id " + instanceId);
    }
}

And testing class:

@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {

    @Rule
    public ActivityTestRule<MainActivity> mActivityTestRule = new ActivityTestRule<>(MainActivity.class);

    @Test
    public void useAppContext() throws Exception {

        MainActivity activity1 = mActivityTestRule.getActivity();
        int act1 = activity1.instanceId.intValue();
        int counter1 = MainActivity.creationCounter;
        assertEquals(1, counter1);
        assertEquals(1, act1);


        Log.d("TEST", "requesting rotation");
        // method 1
         activity1.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
        // method 2 //https://gist.github.com/nbarraille/03e8910dc1d415ed9740#file-orientationchangeaction-java
        // onView(isRoot()).perform(orientationLandscape());

        getInstrumentation().waitForIdleSync(); // I thought this should suffice
        // How to do this?
        //somehowRefreshActivityInstanceInsideTestRule();

        MainActivity activity2 = mActivityTestRule.getActivity();
        int act2 = activity2.instanceId.intValue();
        int counter2 = MainActivity.creationCounter;
        Log.d("TEST", "newly acquired activity " + activity2 + " has id " + act2 + "/" + counter2);

        assertEquals(2, counter2);
        assertEquals(2, act2);
    }
}

Above code (either method1 or 2) gives logcat:

D/ActivityTestRule: Launching activity example.com.rotationtest.MainActivity
D/TEST: creating activity example.com.rotationtest.MainActivity@47404a3, has id 1
D/TEST: requesting rotation
D/TEST: creating activity example.com.rotationtest.MainActivity@169887e, has id 2
D/TEST: newly acquired activity example.com.rotationtest.MainActivity@47404a3 has id 1/2
I/TestRunner: failed: useAppContext(example.com.rotationtest.ExampleInstrumentedTest)
I/TestRunner: ----- begin exception -----
I/TestRunner: java.lang.AssertionError: expected:<2> but was:<1>

My diagnosis, correct me if I'm wrong:

  1. activity1.setRequestedOrientation causes creation of new activity in other thread. I HOPE it would receive proper bundle
  2. getInstrumentation().waitForIdleSync(); causes test to wait until the new activity is created
  3. mActivityTestRule.getActivity(); still returns old activity instance.
  4. I need some way to refresh activity instance held inside test rule, release previously held one.

I found answer with older version of test framework: Instrumentation test for Android - How to receive new Activity after orientation change?

mActivity.setRequestedOrientation(
    ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
mActivity.finish();
setActivity(null);
mActivity = getActivity();
getInstrumentation().waitForIdleSync();

But I don't know how to translate it into new version.

EDIT:

both of methods above leave activity in destroyed state: assertFalse(mActivityTestRule.getActivity().isDestroyed()); fails.

I found another method (Destroy and restart Activity with Testing Support Library) that recreates activity instance, but does not keep its state through onSaveInstanceState


Solution

  • I finally found working solution here: Get Current Activity in Espresso android

    After adapting to my needs code looks like this:

    public class CurrentActivityTestRule<T extends Activity> extends ActivityTestRule<T> {
        public CurrentActivityTestRule(Class<T> activityClass) {
            super(activityClass, false);
        }
    
        public CurrentActivityTestRule(Class<T> activityClass, boolean initialTouchMode) {
            super(activityClass, initialTouchMode, true);
        }
    
        public CurrentActivityTestRule(Class<T> activityClass, boolean initialTouchMode, boolean launchActivity) {
            super(activityClass, initialTouchMode, launchActivity);
        }
    
        public T getCurrentActivity() {
            getInstrumentation().waitForIdleSync();
            final Activity[] activity = new Activity[1];
            getInstrumentation().runOnMainSync(new Runnable() {
                @Override
                public void run() {
                    java.util.Collection<Activity> activities = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED);
                    activity[0] = Iterables.getOnlyElement(activities);
                }});
            T current = (T) activity[0];
            return current;
        }
    }
    

    and is used like this:

    onView(isRoot()).perform(orientationLandscape());
    Activity oldActivityInstance = mActivityTestRule.getActivity();
    Activity currentActivityInstance = mActivityTestRule.getCurrentActivity();
    

    I have this working with library versions:

    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    androidTestCompile('com.android.support.test:runner:0.5', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })