使用仪器和 JUnit4 测试重新创建 Android Activity
Test recreating Android Activity using instrumentation and JUnit4
我想为重新创建编写测试 activity。执行旋转是可选的。
我希望 Google 使用最新版本的测试框架 "blessed" 编写测试。我是编写测试的新手,所以我想学习基本的、主流的、支持良好的工具。当我掌握基础知识时,任何第 3 方测试框架都可以。而且由于我想测试非常基本的、经常出现的场景,基本的工具应该就足够了,对吧?
最小测试代码:
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);
}
}
并测试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);
}
}
以上代码(方法 1 或 2)给出 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>
我的诊断,如有错误请指正:
- activity1.setRequestedOrientation 导致在其他线程中创建新的 activity。我希望它能收到合适的捆绑包
- getInstrumentation().waitForIdleSync();导致测试等到新的activity被创建
- mActivityTestRule.getActivity();仍然是 returns 旧的 activity 实例。
- 我需要一些方法来刷新 activity 测试规则中保存的实例,释放之前保存的一个实例。
我用旧版本的测试框架找到了答案: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();
但是我不知道怎么翻译成新版本。
编辑:
以上两种方法都使 activity 处于销毁状态:assertFalse(mActivityTestRule.getActivity().isDestroyed());失败。
我发现了另一种方法 (Destroy and restart Activity with Testing Support Library) 可以重新创建 activity 实例,但不会通过 onSaveInstanceState
保持其状态
我终于在这里找到了可行的解决方案:Get Current Activity in Espresso android
适应我的需求后代码如下所示:
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;
}
}
并且是这样使用的:
onView(isRoot()).perform(orientationLandscape());
Activity oldActivityInstance = mActivityTestRule.getActivity();
Activity currentActivityInstance = mActivityTestRule.getCurrentActivity();
我使用的是库版本:
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'
})
当前版本的 android.test.rule
解决了这个问题。
您只需要在调用 activityTestRule.getActivity()
之前调用 InstrumentationRegistry.getInstrumentation().waitForIdleSync()
@MateuszL 的解决方案现在仍然适用于 androidx 测试编排器和 Kotlin,以防此版本对任何人有帮助:
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
)
}
...并且在 Kotlin 中是这样使用的,例如,如果我们知道此时在测试中当前 activity 应该是 RegistrationActivity 并且我们需要在一个过程中直接访问它的功能之一集成测试:
@Suppress("CAST_NEVER_SUCCEEDS")
private fun getRegistrationActivity()= activityTestRule.currentActivity as RegistrationActivity
我想为重新创建编写测试 activity。执行旋转是可选的。
我希望 Google 使用最新版本的测试框架 "blessed" 编写测试。我是编写测试的新手,所以我想学习基本的、主流的、支持良好的工具。当我掌握基础知识时,任何第 3 方测试框架都可以。而且由于我想测试非常基本的、经常出现的场景,基本的工具应该就足够了,对吧?
最小测试代码:
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);
}
}
并测试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);
}
}
以上代码(方法 1 或 2)给出 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>
我的诊断,如有错误请指正:
- activity1.setRequestedOrientation 导致在其他线程中创建新的 activity。我希望它能收到合适的捆绑包
- getInstrumentation().waitForIdleSync();导致测试等到新的activity被创建
- mActivityTestRule.getActivity();仍然是 returns 旧的 activity 实例。
- 我需要一些方法来刷新 activity 测试规则中保存的实例,释放之前保存的一个实例。
我用旧版本的测试框架找到了答案: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();
但是我不知道怎么翻译成新版本。
编辑:
以上两种方法都使 activity 处于销毁状态:assertFalse(mActivityTestRule.getActivity().isDestroyed());失败。
我发现了另一种方法 (Destroy and restart Activity with Testing Support Library) 可以重新创建 activity 实例,但不会通过 onSaveInstanceState
保持其状态我终于在这里找到了可行的解决方案:Get Current Activity in Espresso android
适应我的需求后代码如下所示:
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;
}
}
并且是这样使用的:
onView(isRoot()).perform(orientationLandscape());
Activity oldActivityInstance = mActivityTestRule.getActivity();
Activity currentActivityInstance = mActivityTestRule.getCurrentActivity();
我使用的是库版本:
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'
})
当前版本的 android.test.rule
解决了这个问题。
您只需要在调用 activityTestRule.getActivity()
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
@MateuszL 的解决方案现在仍然适用于 androidx 测试编排器和 Kotlin,以防此版本对任何人有帮助:
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
)
}
...并且在 Kotlin 中是这样使用的,例如,如果我们知道此时在测试中当前 activity 应该是 RegistrationActivity 并且我们需要在一个过程中直接访问它的功能之一集成测试:
@Suppress("CAST_NEVER_SUCCEEDS")
private fun getRegistrationActivity()= activityTestRule.currentActivity as RegistrationActivity