Android单元测试总结

当年做Launcher单元测试的总结的一些要点,回顾一下

简述

参考资料:

Android自动化测试–学习浅谈
Android单元测试实践
Android单元测试 - 如何开始?
Android单元测试系列: java的单元测试比较详细,有MVP和一些流行框架的测试方法

Android有关的单元测试大体分为两类:

  1. 本地单元测试

    • 运行在jvm上的测试框架,不需要Android环境
    • 位于src/test/java
    • gradle引入时使用testCompile
    • Junit4MockitoPowermockitoRobolectric
  2. Android Instrumentation测试

    • 运行在Android环境上的测试框架,依赖真机或都模拟器环境
    • 代码位于src/androidTest/java
    • gradle引入时使用androidTest
      AndroidJUnitRunnerEspresso, UI Automator

各种框架简介:

  1. Junit4: 基础的Java单元测试
  2. Mockito: 模拟测试的类,是一个工具类的集合,配置其它框架使用
  3. Robolectric: JVM环境中模拟Android的环境,可以在不连接Android设备的情况下进行测试。听起来很美好,但使用起来不是很方便,还一堆坑,介绍文章: https://www.jianshu.com/p/d0bc9ebaaea1
  4. Espresso: UI测试,适合白盒测试
  5. UI Automator: UI测试,适合黑盒测试,测试组的自动化脚本应该就是基于这个写的

框架结构

使用Android Instrumentation测试,测试代码写在src/androidTest/java目录下

使用到的框架有junit4, mockito, Instrumentation, uiautomator, espresso
gradle下的依赖方式:

1
2
3
4
5
6
7
8
9
10
defaultConfig {
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}

//dependences
androidTestCompile libraries.mockitoAndroid
androidTestCompile libraries.testRunner
androidTestCompile libraries.uiautomator
androidTestCompile libraries.annotations
androidTestCompile libraries.espresso

各框架的基本使用

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
2
3
4
5
6
7
8
9
10
Mock LauncherAppState mMockApp;
Mock IconCache mMockIconCache;

public void setUp() throws Exception {
super.setUp();
MockitoAnnotations.initMocks(this);

when(mMockApp.getIconCache()).thenReturn(mMockIconCache);
when(mMockApp.getContext()).thenReturn(mTargetContext);
}

uiautomator

主要进行UI测试,能够通过Id, text和description来查找元素,不限进程,使用方便,确点是只能获取到当前显示的最顶层Window上的view,对应着sdk下的uiautomatorviewer工具。

这个在自动化测试时也是一个重要的工具,可以针对Release版本进行测试,只是使用的jar包和shell脚本的方式做的。

使用时例如:
UI Automator的Api主要包括三个方面 : UiDevice, UiObject/UiObject2, UiSelector/BySelector, UiObjectUiObject2功能一样,只是查找用的Selector不一样,一般都用比较方便的BySelectorUiObject2,例如:

1
2
3
UiDevice mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
UiObject2 button = mDevice.findObject(By.text("text"))
button.click()

Espresso

uiautomator虽然使用很方便,但是还是有很多局限,无法获取到一些View的属性,也只能通过Assert来判断。这时就可以结合Espresso使用
基本使用方法: https://www.jianshu.com/p/37f1897df3fd

下面是官方网站给出的一个例子,

1
2
3
onView(withId(R.id.my_view))            
.perform(click())
.check(matches(isDisplayed()));

可以自定义Matcher来检查更为复杂的属性

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
final InvariantDeviceProfile idp = SettingDeviceProfile.getProfileById(gridSizeIds[index]);

onView(withId(R.id.workspace)).check(matches(new TypeSafeMatcher<View>() {

Point reallySize = new Point();
Point targetSize = new Point();

@Override
protected boolean matchesSafely(View item) {
if(!(item instanceof Workspace)){
return false;
}
Workspace workspace = (Workspace) item;
CellLayout page = workspace.getCurrentDropLayout();
reallySize.set(page.getCountX(), page.getCountY());
targetSize.set(idp.numColumns, idp.numRows);
return page.getCountX() == idp.numColumns && page.getCountY() == idp.numRows;
}

@Override
public void describeTo(Description description) {
description.appendText(" gridSizeId is ")
.appendText(String.valueOf(idp.gridSizeId))
.appendText(", target is")
.appendText(targetSize.toString())
.appendText(", really is ")
.appendText(reallySize.toString());
}
}));

开发要点:

  1. 测试的包括两方面: 纯功能测试和UI测试。

纯功能测试不要涉及Ui,不能启动Launcher,也不能使用uiautomatorespresso,代码统一放在com.transsion.launcher.fun包下
Ui测试,才uiautomator, espresso为辅,写到com.transsion.launcher.ui

  1. 所有测试均继承BaseContextAndroidTest,使用这个基类提供的mTargetContextmTargetPackage,要注意需要包名时不要使用BuildConfig.APPLICATION_ID,要使用基类提供的mTargetPackage

  2. 所有需要测试的类前面要加上@RunWith(AndroidJUnit4.class)的注解,测试方法加上@Test的注意,且必需为public的

  3. 有异常可在方法上加上throws Exception上抛出,尽量不要使用try catch

  4. 多使用assert进行断言

  5. 需要Launcher对象,可以定义新建LauncherActivityRule后获取,例如

1
2
3
4
5
6
 @Rule
public LauncherActivityRule mActivityMonitor = new LauncherActivityRule();
@Test
public void findFreezerIcon(){
mActivityMonitor.getActivity()
}
  1. 需要等待时使用waitForIdle或者Wait.sleep();
  2. Wait.atMost()可以等待某一条件成立
  3. uiautomator只能检查有限的属性,可检查的属性可从sdk/tools/uiautomatorviewer.bat来看
  4. 异步Api可通过CountDownLatch来等待

CountDownLatch是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作执行完后 再 执行。
CountDownLatch提供countDown() 方法递减锁存器的计数,如果计数到达零,则释放所有等待的线程,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void testLoadWallpaper() throws InterruptedException {
final CountDownLatch latch = new CountDownLatch(1);
final ArrayList<LocalWallpaperInfo> wallpaperInfos = new ArrayList<>();
GuideLoadPresenter.GuideUi ui = new GuideLoadPresenter.GuideUi() {
@Override
public void loadComplete(ArrayList<LocalWallpaperInfo> infos, Drawable previewIcon) {
wallpaperInfos.addAll(infos);
latch.countDown();
}
};
GuideLoadPresenter presenter = new GuideLoadPresenter(ui);
presenter.startLoad(mTargetContext, false);
latch.await(20, TimeUnit.SECONDS);
Assert.assertNotNull(ui);//GuideLoadPresenter内的ui是弱引用
Assert.assertThat(wallpaperInfos.size(), anyOf(is(1), is(2)));
}

测试点

纯功能测试:
由于Launcher各功能偶合性太强,可以写的测试用例有限,很多功能需要解偶才能写,这里建议多写工具方法的测试,如检测壁纸的util方法:

  1. AppLaunchCountRecorder 测试记录常用应用功能是否正常
  2. GuideLoadPresenter 测试是否能够正确获取壁纸信息
  3. LoadCursor 能否正常解析cursor中的数据
  4. WallpaperUtils 测试能否正常获取壁纸及检查壁纸深浅

Ui测试,才uiautomator, espresso为辅

  1. Settings界面

    • 设置界面桌面提示
    • 修改图标大小
    • 选择桌面网格
    • 图标锁定点击开关,相关设置是否禁用
    • A-Z界面移动方向切换,是否生效
    • 文件颜色切换,是否生效
    • 智能整理
    • 关于XOS桌面几项功能跳转
  2. AllApp界面

    • 切换横向,竖向
    • 竖向界面竖向滑动
    • 竖向界面检查常用应用一栏
    • 竖向界面是否有搜索框,点击搜索框能否弹出输入法
    • 图标长按能否弹出菜单,发送到桌面
  3. 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
77
public 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等
*/
@Before
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);
}
}