活动生命周期单元测试

21

在一个活动单元测试中,如何模拟活动生命周期事件。

我可以调用现有活动的instrumentation的callActivityOn...方法,但是如何触发活动重新创建,以便活动的OnCreate得到保存状态的Bundle。

7个回答

9
我发现这段代码会创建一个新的Activity:
myActivity.finish();
setActivity(null);
myActivity = getActivity();

但这并不会导致调用 onSaveInstanceState。 因此,例如,要测试视图方向更改后活动是否正确创建,应该执行以下测试:
private mInstrumentation = getInstrumentation();
...
final Bundle outState = new Bundle();
mInstrumentation.callActivityOnSaveInstanceState(mActivity, outState);
mActivity.finish();
setActivity(null);
mActivity = getActivity();
runTestOnUiThread(new Thread() {
    @Override
    public void run() {
        mInstrumentation.callActivityOnRestoreInstanceState(mActivity, outState);
    }
});

6
不要跟随状态管理测试示例:{无效链接}
myActivity.finish();
myActivity = getActivity();
ActivityInstrumentationTestCase2.getActivity()在第一次调用时启动Activity,但在后续的测试用例中,它只是返回同一个Activity。因此,你仍然在查看已经完成的Activity。

在完成第一个Activity之后,你需要从测试中启动一个新的Activity。例如,你可以使用InstrumentationTestCase.launchActivity()

另外,我编写了一个测试用例,在ActivityA中按下按钮,启动ActivityB以获取结果;然后,测试立即杀死ActivityA(通过方向更改,但也可以使用finish()),然后测试获得系统在ActivityB完成并发送其结果时创建的新ActivityA的句柄。其中的技巧是让测试添加一个Instrumentation.ActivityMonitor,然后让该监视器等待系统启动新的ActivityA并将其句柄传递给测试。

编辑于2/23/2012 cdhabecker,添加可重现的代码:

public class VerboseActivity extends Activity {
    public final static String TAG = "Verbose";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        Log.i(TAG, "onCreate() " + (Activity)this);
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity5);
    }
    @Override
    protected void onDestroy() {
        Log.i(TAG, "onDestroy().");
        super.onDestroy();
    }
}

测试用例:(sleep()调用为活动提供了足够的响应时间)

public class VerboseTest extends
        ActivityInstrumentationTestCase2<VerboseActivity> {

    Activity myActivity = null;

    public VerboseTest() {
        super("com.scanillion.demo", VerboseActivity.class);
    }

    public void test_01() {
        String TAG = "test_01";
        myActivity = getActivity();
        Log.i(TAG, "A getActivity()=" + myActivity);
        myActivity.finish();
        try {
            Thread.sleep(5000L);
        } catch (InterruptedException e) {
        }
        myActivity = getActivity();
        Log.i(TAG, "B getActivity()=" + myActivity);
        try {
            Thread.sleep(5000L);
        } catch (InterruptedException e) {
        }
    }
}

日志:

02-23 21:25:37.689: I/Verbose(17747): onCreate() com.scanillion.demo.VerboseActivity@43ba3360
02-23 21:25:38.159: I/ActivityManager(67): Displayed activity com.scanillion.demo/.VerboseActivity: 526 ms (total 526 ms)
02-23 21:25:38.180: I/test_01(17747): A getActivity()=com.scanillion.demo.VerboseActivity@43ba3360
02-23 21:25:38.540: I/Verbose(17747): onDestroy().
02-23 21:25:43.236: I/test_01(17747): B getActivity()=com.scanillion.demo.VerboseActivity@43ba3360
02-23 21:25:48.439: I/TestRunner(17747): finished: test_01(com.scanillion.demo.test.VerboseTest)
02-23 21:25:48.439: I/TestRunner(17747): passed: test_01(com.scanillion.demo.test.VerboseTest)

注意,finish() 会导致 onDestroy() 被调用,但随后的 getActivity() 操作没有产生任何效果。不仅 getActivity() 不会实例化新的 Activity,它甚至不会重新创建原始的 Activity。

你能提供可靠的参考来支持你的说法“在测试用例中,每次调用都会简单地返回相同的Activity”吗?顺便说一句,我认为这里的关键点是通过调用Activity.finish()来完全销毁具有用户输入状态的Activity,我的答案中的测试代码用于检查是否正确实现了保存活动状态在Activity.onCreate()中,不管返回的是相同的实例(我根据你的陈述假设,尽管状态不同)。 - yorkw
2
@yorkw 我添加了一个可重现的案例。 - cdhabecker
我仍然不明白为什么 Thread.sleep(5000L)finish() 后导致了 onStop() > onDestroy() 的调用。对我来说,sleep() 技巧很有效,因为我期望 onStop() 和 onDestroy() 被调用。 - Thuy Trinh

3

我确认 cdhabecker 是正确的,getActivity() 返回在开始时创建的活动,即使你“结束”了它。但是我认为我已经找到了测试活动重建的解决方案。您可以尝试请求方向更改。这将重新创建您的活动,然后您检索新创建的活动。下面是代码片段:(我使用了robotium):

protected void setUp() throws Exception {
  super.setUp();
  mActivity = getActivity();
  mSolo = new Solo(getInstrumentation(), getActivity());
  Log.v(TAG, "setUp; activity=" + mActivity);
}

public void testOrienationChange(){     
  mSolo.setActivityOrientation(Solo.LANDSCAPE);
  getInstrumentation().waitForIdleSync();
  MyActivity newActivity = getActivity(); //should be new, but it's not
  Activity newActivity2 = mSolo.getCurrentActivity(); //this will return newly created
  Log.v(TAG, "testOrienationChange; activity=" + newActivity);
  Log.v(TAG, "testOrienationChange; activity2=" + newActivity2);
}   

当然,如果您防止活动在方向更改后被销毁,它将无法工作。您可以在这里找到我的完整答案,其中包括日志消息。希望这有所帮助。问候!

1

在官方开发指南中,有一个非常好的例子讲述了状态管理测试这里。基本上,您只需要调用Activity.finish()来模拟活动已被杀死,可以查看下面的伪代码:

public void testIfStateIsSaved() {
  // Open myActivity first time.
  MyActivity myActivity = getActivity();
  final EditText editText = (EditText) myActivity.findViewById(com.company.R.id.edit_text);
  // emulate some user action
  myActivity.runOnUiThread(new Runnable() {
    public void run() {
      editText.setText("save me");
    }
  });

  // Suppose you have implemented saved state properly.

  // kill activity and restart it again.
  myActivity.finish();
  myActivity = getActivity();
  final EditText editText2 = (EditText) myActivity.findViewById(com.company.R.id.edit_text);
  assertEquals("user input must be saved", "save me", editText2.getText());
}

希望这能有所帮助。

4
我认为这个答案不起作用 -- 你会发现getActivity()并没有启动一个新的活动 -- 它是你之前拥有的相同的活动。请参考 https://dev59.com/Smox5IYBdhLWcg3wJxH4#9422946 获取另一种方法。 - cdhabecker
1
我最初认为这是答案,结果证明我错了 - 请参见@cdhabecker的评论。 - mfeingold

1

对cdhabecker的回答进行详细说明,我创建了以下静态方法,它适用于我:

public static Activity restartActivity(Activity activity, Instrumentation instrumentation, Intent intent){
    String className = activity.getClass().getName();
    Instrumentation.ActivityMonitor monitor = instrumentation.addMonitor(className, null, false);
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    intent.setClassName(instrumentation.getTargetContext(), className );
    instrumentation.startActivitySync(intent);
    Activity newActivity = instrumentation.waitForMonitor(monitor);
    instrumentation.removeMonitor(monitor);
    return newActivity;
}

使用完活动后,我通过调用destroy()方法将其销毁并重置。

activity.finish();
setActivity(null);

在我的ActivityInstrumentationTestCase2类中。

1
如果您有一台Android 4.x设备,您可以进入“设置”>“开发者选项”,并勾选“不保留活动”。现在,每当您的Activity失去焦点(例如:HOME按钮),它将被杀死,并调用onSaveInstanceState(...)。
当您恢复应用程序时,如果您在onSaveInstanceState(...)中保存了数据,则您的Activity应该在onCreate(...)方法中具有bundle数据。

3
这可能适用于手动测试。我正在寻找一种在自动化测试中实现的方法。 - mfeingold

0

你可以通过ActivityLifeCycleMonitor获取新的恢复的Activity。

例如,这个方法会等待并将新创建的Activity设置为当前的Activity。

public void waitAndSetResumedActivity() {
    // well at least there are some activities in the pipeline - lets see if they resume.

    long[] waitTimes =
            {10, 50, 100, 500, TimeUnit.SECONDS.toMillis(2), TimeUnit.SECONDS.toMillis(30)};

    final ActivityLifecycleMonitor activityLifecycleMonitor = ActivityLifecycleMonitorRegistry.getInstance();
    final AtomicBoolean activityResumed = new AtomicBoolean(false);
    for (int waitIdx = 0; waitIdx < waitTimes.length; waitIdx++) {
        if (activityResumed.get()) return;
        try {
            Thread.sleep(waitTimes[waitIdx]);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        getInstrumentation().runOnMainSync(new Runnable() {
            @Override
            public void run() {
                Collection<Activity> resumedActivities = activityLifecycleMonitor.getActivitiesInStage(Stage.RESUMED);
                if (!resumedActivities.isEmpty()) {
                    activity = (MainActivity) resumedActivities.iterator().next();
                    setActivity(activity);
                    activityResumed.set(true);
                }
            }
        });

    }
    throw new NoActivityResumedException("No activities in stage RESUMED. Did you forget to "
            + "launch the activity. (test.getActivity() or similar)?");

}

因此,在调用此方法后,任何对 getActivity() 的调用都将返回新的 Activity。

您可以通过以下方式测试旋转时的 Activity 重建:

Activity activity = getActivity(); // old activity    
//rotate it
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
//set new Activity
waitAndSetResumedActivity();
activity = getActivity();  // New Activity

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接