Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 387
MateuszL Avatar asked Feb 27 '17 09:02

MateuszL


2 Answers

@MateuszL's solution still works now with the androidx test orchestrator and Kotlin, in case this version helps anyone:

import androidx.test.espresso.core.internal.deps.guava.collect.Iterables
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import androidx.test.rule.ActivityTestRule
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry
import androidx.test.runner.lifecycle.Stage

class CurrentActivityTestRule<T : Activity> : ActivityTestRule<T> {

    val currentActivity: T
        get() {
            getInstrumentation().waitForIdleSync()
            val activity = arrayOfNulls<Activity>(1)
            getInstrumentation().runOnMainSync(Runnable {
                val activities = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED)
                activity[0] = Iterables.getOnlyElement(activities)
            })
            @Suppress("UNCHECKED_CAST")
            return activity[0] as T
        }

    constructor(activityClass: Class<T>) : super(activityClass, false)

    constructor(activityClass: Class<T>, initialTouchMode: Boolean) : super(activityClass, initialTouchMode, true)

    constructor(activityClass: Class<T>, initialTouchMode: Boolean, launchActivity: Boolean) : super(
        activityClass,
        initialTouchMode,
        launchActivity
    )
}

...and is used in Kotlin like this, for example if we know that at this point in the test the current activity should be RegistrationActivity and we need to access one of its functions directly during an integration test:

@Suppress("CAST_NEVER_SUCCEEDS")
private fun getRegistrationActivity()= activityTestRule.currentActivity as RegistrationActivity
like image 75
ChrisPrime Avatar answered Sep 29 '22 22:09

ChrisPrime


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'
})
like image 38
MateuszL Avatar answered Sep 29 '22 23:09

MateuszL