前言

UI单元测试使用官方的Espresso
在开发中对于重要的功能可编写单元测试,为防止后期的修改影响功能,每次开发完跑一遍测试即可保证功能的完整性

  1. 点击change按钮:设置内容到TextView上
  2. 点击open按钮:打开一个Activity,同时把内容传递过去,用来显示到TextView上

    Espresso依赖

    1
    2
    3
    4
    5
    6
    7
    8
    dependencies {
    //test
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test:runner:1.1.0'
    androidTestImplementation 'androidx.test:rules:1.1.0'
    //espresso
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
    }

环境依赖

1
2
3
4
5
android {
defaultConfig {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
}

添加单元测试任务

  1. Run>Edit Configurations
  2. 添加一个Android Instrumented Tests
  3. 选择对应的module
  4. 选择真机或者模拟器
  5. 如果选择真机:关闭开发者选项>绘画>窗口动画缩放;过滤动画缩放;动画程序时长缩放

    编写单元测试

    我们先验证应用是否开启了,直接验证是否有Hello World
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @RunWith(AndroidJUnit4.class)
    @LargeTest
    public class ChangeTextBehaviorTest {

    @Rule
    public ActivityTestRule<MainActivity> activityRule
    = new ActivityTestRule<>(MainActivity.class, false, true);

    @Test
    public void listGoesOverTheFold() {
    onView(withText("Hello world!")).check(matches(isDisplayed()));
    }
    }
  • 其中@LargeTest可根据自己情况来改变,具体见下图
    链接图片
  • @Rule定义测试启动的Activity
  • @Test来测试方法
  • 常用的Espresso的API
  1. onView 查找元素;onData() 查找AdapterView元素

    withText()通过文字查找

    withId()通过id查找
    allOf()匹配多个条件-org.hamcrest.Matchers
  2. perform 执行操作

    click点击

    typeText点击并且输入一个值;最好结合closeSoftKeyboard
    scrollTo滑动-onView必须是ScrollView
    pressKey按键
    clearText清空view的文字
  3. check 验证结果

    matches

  4. 所有的API可见官方给出的图示
    链接图片

如果我们启动的Activity不是MainActivity,而且需要intent传值,则可以使用下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RunWith(AndroidJUnit4.class)
@LargeTest
public class TextShowTextActivity {

private static final String MESSAGE = "12312";

@Rule
public ActivityTestRule<ShowTextActivity> activityRule
= new ActivityTestRule<ShowTextActivity>(ShowTextActivity.class, false, true){
@Override
protected Intent getActivityIntent() {
Intent result = new Intent();
result.putExtra(ShowTextActivity.KEY_EXTRA_MESSAGE, MESSAGE);
return result;
}
};

@Test
public void listGoesOverTheFold() {
onView(withText(MESSAGE)).check(matches(isDisplayed()));
}

}

使用intent单独测试Activity

即可以获取即将打开Activity的intent来检查一个Activity的完成性

  • 官方指南

  • 依赖

    1
    2
    3
    4
    dependencies {
    //intent
    androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.0'
    }
  • 改变Rule

    1
    2
    @Rule
    public IntentsTestRule<MyActivity> intentsTestRule = new IntentsTestRule<>(MyActivity.class);

测试代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@RunWith(JUnit4.class)
@LargeTest
public class ChangePageBehaviorTest {

private static final String MESSAGE = "This is a test";
private static final String PACKAGE_NAME = "com.mlc.android_test";

@Rule
public IntentsTestRule<MainActivity> intentsRule =
new IntentsTestRule<>(MainActivity.class);
@Test
public void verifyMessageSentToMessageActivity() {

// Types a message into a EditText element.
onView(withId(R.id.editTextUserInput))
.perform(typeText(MESSAGE), closeSoftKeyboard());

// Clicks a button to send the message to another
// activity through an explicit intent.
onView(withId(R.id.activityChangeTextBtn)).perform(click());

// Verifies that the DisplayMessageActivity received an intent
// with the correct package name and message.
intended(allOf(
hasComponent(hasShortClassName(".ShowTextActivity")),
toPackage(PACKAGE_NAME),
hasExtra(ShowTextActivity.KEY_EXTRA_MESSAGE, MESSAGE)));
}
}

UiAutomator

UIAutomator主要用于多个应用之间的测试
由于目前没有好的例子,Demo中也只是使用了官方的用例,这里只给出链接

  • 官方指南

    测试原理浅析

    首先我们需要了解Activity的开启流程,可以参考我总结的Activity启动流程
    Activity需要通过Instrumentation来与系统交互的,单元测试中其实也一样,通过它来开启Activity
    我们从ActivityTestRule来作为入口
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    #ActivityTestRule
    /**
    * Launches the Activity under test.
    */
    public T launchActivity(@Nullable Intent startIntent) {
    //...
    if (null == startIntent) {
    startIntent = getActivityIntent();
    if (null == startIntent) {
    startIntent = new Intent(Intent.ACTION_MAIN);
    }
    }
    if (null == startIntent.getComponent()) {
    startIntent.setClassName(targetPackage, activityClass.getName());
    }
    T hardActivityRef = activityClass.cast(instrumentation.startActivitySync(startIntent));
    //...
    return hardActivityRef;
    }

启动Activity是通过launchActivity()方法来启动,看做了什么操作:

  1. 设置intent
  2. 使用instrumentation.startActivitySync()开启Activity
    到这里我们已经清楚单元测试也与普通的应用一样,是用instrumentation来开启Activity

    题外话

    其实这个instrumentation是MonitoringInstrumentation
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    #MonitoringInstrumentation

    @Override
    public Activity startActivitySync(final Intent intent) {
    checkNotMainThread();
    Future<Activity> startedActivity =
    executorService.submit(
    new Callable<Activity>() {
    @Override
    public Activity call() {
    return MonitoringInstrumentation.super.startActivitySync(intent);
    }
    });

    try {
    return startedActivity.get(START_ACTIVITY_TIMEOUT_SECONDS, TimeUnit.SECONDS);
    } catch (TimeoutException te) {
    dumpThreadStateToOutputs("ThreadState-startActivityTimeout.txt");
    startedActivity.cancel(true);
    throw new RuntimeException(
    String.format(
    "Could not launch intent %s within %s seconds."
    + " Perhaps the main thread has not gone idle within a reasonable amount of "
    + "time? There could be an animation or something constantly repainting the "
    + "screen. Or the activity is doing network calls on creation? See the "
    + "threaddump logs. For your reference the last time the event queue was idle "
    + "before your activity launch request was %s and now the last time the queue "
    + "went idle was: %s. If these numbers are the same your activity might be "
    + "hogging the event queue.",
    intent,
    START_ACTIVITY_TIMEOUT_SECONDS,
    lastIdleTimeBeforeLaunch,
    lastIdleTime.get()));
    } catch (ExecutionException ee) {
    throw new RuntimeException("Could not launch activity", ee.getCause());
    } catch (InterruptedException ie) {
    Thread.currentThread().interrupt();
    throw new RuntimeException("interrupted", ie);
    }
    }

我们看到是通过线程池来开启,获取Future后,设置了超时时间45s。在我们设置activity有问题时经常出现

Tips

  • 真机运行可能需要安装应用后开启手机允许后台弹出界面权限