当年做Launcher单元测试的总结的一些要点,回顾一下
简述
参考资料:
Android自动化测试–学习浅谈
Android单元测试实践
Android单元测试 - 如何开始?
Android单元测试系列: java的单元测试比较详细,有MVP和一些流行框架的测试方法
Android有关的单元测试大体分为两类:
本地单元测试
- 运行在jvm上的测试框架,不需要Android环境
- 位于
src/test/java
- gradle引入时使用testCompile
- 有
Junit4
、Mockito
、Powermockito
,Robolectric
Android Instrumentation测试
- 运行在Android环境上的测试框架,依赖真机或都模拟器环境
- 代码位于
src/androidTest/java
- gradle引入时使用
androidTest
有AndroidJUnitRunner
,Espresso
,UI Automator
各种框架简介:
Junit4
: 基础的Java单元测试Mockito
: 模拟测试的类,是一个工具类的集合,配置其它框架使用Robolectric
: JVM环境中模拟Android
的环境,可以在不连接Android设备的情况下进行测试。听起来很美好,但使用起来不是很方便,还一堆坑,介绍文章: https://www.jianshu.com/p/d0bc9ebaaea1Espresso
: UI测试,适合白盒测试UI Automator
: UI测试,适合黑盒测试,测试组的自动化脚本应该就是基于这个写的
框架结构
使用Android Instrumentation
测试,测试代码写在src/androidTest/java
目录下
使用到的框架有junit4
, mockito
, Instrumentation
, uiautomator
, espresso
gradle
下的依赖方式:
1 | defaultConfig { |
各框架的基本使用
Junit4
主要提供各种注解和断言
最重要的注解@Test
表明了测试的方式,根据根据方法上的注解,其执行顺序为@BeforeClass –> @Before –> @Test –> @After –> @AfterClass
断言一般常用assertNotNull, assertEquals等
详细Api和使用见此博客:https://blog.csdn.net/qq_17766199/article/details/78243176
mockito
用来模拟对象,脱离对Android Api
依赖
详细使用方法见: https://blog.csdn.net/qq_17766199/article/details/78450007
对于一些依赖的流程过多的测试可以用该方法隔离依赖: 例如LauncherAppState的初始化是在Launcher启动时,在样在测试依赖LauncherAppState
的代码时就可以用mock来模拟对象,如:
1 | Mock LauncherAppState mMockApp; |
uiautomator
主要进行UI测试,能够通过Id, text和description来查找元素,不限进程,使用方便,确点是只能获取到当前显示的最顶层Window上的view,对应着sdk下的uiautomatorviewer
工具。
这个在自动化测试时也是一个重要的工具,可以针对Release版本进行测试,只是使用的jar包和shell脚本的方式做的。
使用时例如:
UI Automator的Api主要包括三个方面 : UiDevice
, UiObject/UiObject2
, UiSelector/BySelector
, UiObject
和UiObject2
功能一样,只是查找用的Selector
不一样,一般都用比较方便的BySelector
和UiObject2
,例如:
1 | UiDevice mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); |
Espresso
uiautomator
虽然使用很方便,但是还是有很多局限,无法获取到一些View的属性,也只能通过Assert来判断。这时就可以结合Espresso
使用
基本使用方法: https://www.jianshu.com/p/37f1897df3fd
下面是官方网站给出的一个例子,
1 | onView(withId(R.id.my_view)) |
可以自定义Matcher
来检查更为复杂的属性
1 | final InvariantDeviceProfile idp = SettingDeviceProfile.getProfileById(gridSizeIds[index]); |
开发要点:
- 测试的包括两方面: 纯功能测试和UI测试。
纯功能测试不要涉及Ui,不能启动
Launcher
,也不能使用uiautomator
和espresso
,代码统一放在com.transsion.launcher.fun
包下
Ui测试,才uiautomator
,espresso
为辅,写到com.transsion.launcher.ui
下
所有测试均继承
BaseContextAndroidTest
,使用这个基类提供的mTargetContext
和mTargetPackage
,要注意需要包名时不要使用BuildConfig.APPLICATION_ID
,要使用基类提供的mTargetPackage
所有需要测试的类前面要加上
@RunWith(AndroidJUnit4.class)
的注解,测试方法加上@Test
的注意,且必需为public的有异常可在方法上加上
throws Exception
上抛出,尽量不要使用try catch
多使用assert进行断言
需要Launcher对象,可以定义新建
LauncherActivityRule
后获取,例如
1 |
|
- 需要等待时使用
waitForIdle
或者Wait.sleep();
Wait.atMost()
可以等待某一条件成立uiautomator
只能检查有限的属性,可检查的属性可从sdk/tools/uiautomatorviewer.bat
来看- 异步Api可通过CountDownLatch来等待
CountDownLatch
是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作执行完后 再 执行。CountDownLatch
提供countDown()
方法递减锁存器的计数,如果计数到达零,则释放所有等待的线程,例如:
1 |
|
测试点
纯功能测试:
由于Launcher
各功能偶合性太强,可以写的测试用例有限,很多功能需要解偶才能写,这里建议多写工具方法的测试,如检测壁纸的util方法:
AppLaunchCountRecorder
测试记录常用应用功能是否正常GuideLoadPresenter
测试是否能够正确获取壁纸信息LoadCursor
能否正常解析cursor中的数据WallpaperUtils
测试能否正常获取壁纸及检查壁纸深浅
Ui测试,才uiautomator
, espresso
为辅
Settings
界面- 设置界面桌面提示
- 修改图标大小
- 选择桌面网格
- 图标锁定点击开关,相关设置是否禁用
- A-Z界面移动方向切换,是否生效
- 文件颜色切换,是否生效
- 智能整理
- 关于XOS桌面几项功能跳转
AllApp界面
- 切换横向,竖向
- 竖向界面竖向滑动
- 竖向界面检查常用应用一栏
- 竖向界面是否有搜索框,点击搜索框能否弹出输入法
- 图标长按能否弹出菜单,发送到桌面
Launcher桌面
- 页面批量器, 检查是否与桌面页数匹配
- 拖动图标创建文件夹
- 拖动图标到文件夹
- 文件夹内左右滑动
- 文件夹重命名
- 点击打开文件夹添加界面
- 添加界面添加图标
- 长按空白处进入编辑模式
- 是否有冷藏室
- 冷藏室,冷藏解冻
- 安装,卸载应用(内置一个test的apk测试)
附录
Activity基础测试类: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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77public class BaseUiAndroidTest extends BaseContextAndroidTest {
public static final long DEFAULT_UI_TIMEOUT = 1000L;
public static final long DEFAULT_WAITTING_TIMEOUT = 5000L;
protected Context mTargetContext;
protected String mTargetPackage;
UiDevice mDevice;
/**
* 初始化Context, Package等
*/
public void setUp() throws Exception {
mTargetContext = InstrumentationRegistry.getTargetContext();
mTargetPackage = mTargetContext.getPackageName();
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
}
protected String getText(@StringRes int resId){
return mTargetContext.getResources().getString(resId);
}
//启动Activity,且等打开完毕
public void startActivitySync(@NonNull Intent intent){
mTargetContext.startActivity(intent);
waitForIdle();
}
public void pressBack(){
mDevice.pressBack();
}
public void waitForIdle(){
mDevice.waitForIdle();
}
//通过id查找
protected UiObject2 findViewById(@IdRes int id) {
return mDevice.wait(Until.findObject(getSelectorForId(id)), DEFAULT_UI_TIMEOUT);
}
protected UiObject2 findViewByText(@StringRes int resId){
return findViewBySelector(By.text(getText(resId)));
}
protected BySelector getSelectorForId(int id) {
String name = mTargetContext.getResources().getResourceEntryName(id);
return By.res(mTargetPackage, name);
}
protected UiObject2 findViewBySelector(BySelector condition){
return mDevice.wait(Until.findObject(condition), DEFAULT_UI_TIMEOUT);
}
//滚动查找元素
protected UiObject2 scrollAndFind(UiObject2 container, BySelector condition) {
do {
UiObject2 widget = container.findObject(condition);
if (widget != null) {
return widget;
}
} while (container.scroll(Direction.DOWN, 1f));
return container.findObject(condition);
}
protected UiObject2 scrollPageAndFind(UiObject2 container, BySelector condition) {
do {
UiObject2 widget = container.findObject(condition);
if (widget != null) {
return widget;
}
} while (container.scroll(Direction.RIGHT, 1f));
return container.findObject(condition);
}
}