Android单元测试-UI
前言
UI单元测试使用官方的Espresso
在开发中对于重要的功能可编写单元测试,为防止后期的修改影响功能,每次开发完跑一遍测试即可保证功能的完整性
- 友情链接
官网指南
Google官方测试Sample地址
本文demoEspresso
本文demo使用官网的内容demo内容介绍
MainActivity输入框输入文字后
- 点击change按钮:设置内容到TextView上
- 点击open按钮:打开一个Activity,同时把内容传递过去,用来显示到TextView上
Espresso依赖
1
2
3
4
5
6
7
8dependencies {
//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 | android { |
添加单元测试任务
- Run>Edit Configurations
- 添加一个Android Instrumented Tests
- 选择对应的module
- 选择真机或者模拟器
- 如果选择真机:关闭开发者选项>绘画>窗口动画缩放;过滤动画缩放;动画程序时长缩放
编写单元测试
我们先验证应用是否开启了,直接验证是否有Hello World1
2
3
4
5
6
7
8
9
10
11
12
13(AndroidJUnit4.class)
public class ChangeTextBehaviorTest {
public ActivityTestRule<MainActivity> activityRule
= new ActivityTestRule<>(MainActivity.class, false, true);
public void listGoesOverTheFold() {
onView(withText("Hello world!")).check(matches(isDisplayed()));
}
}
- 其中@LargeTest可根据自己情况来改变,具体见下图
- @Rule定义测试启动的Activity
- @Test来测试方法
- 常用的Espresso的API
- onView 查找元素;onData() 查找AdapterView元素
withText()通过文字查找
withId()通过id查找 allOf()匹配多个条件-org.hamcrest.Matchers
- perform 执行操作
click点击
typeText点击并且输入一个值;最好结合closeSoftKeyboard scrollTo滑动-onView必须是ScrollView pressKey按键 clearText清空view的文字
- check 验证结果
matches
- 所有的API可见官方给出的图示
如果我们启动的Activity不是MainActivity,而且需要intent传值,则可以使用下面的代码
1 | (AndroidJUnit4.class) |
使用intent单独测试Activity
即可以获取即将打开Activity的intent来检查一个Activity的完成性
依赖
1
2
3
4dependencies {
//intent
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.0'
}改变Rule
1
2
public IntentsTestRule<MyActivity> intentsTestRule = new IntentsTestRule<>(MyActivity.class);
测试代码如下
1 | (JUnit4.class) |
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()方法来启动,看做了什么操作:
- 设置intent
- 使用instrumentation.startActivitySync()开启Activity
到这里我们已经清楚单元测试也与普通的应用一样,是用instrumentation来开启Activity题外话
其实这个instrumentation是MonitoringInstrumentation1
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
public Activity startActivitySync(final Intent intent) {
checkNotMainThread();
Future<Activity> startedActivity =
executorService.submit(
new Callable<Activity>() {
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
- 真机运行可能需要安装应用后开启手机允许后台弹出界面权限