Android单元测试框架

建议各类测试所占比例如下:小型测试(单元测试)占 70%,中型测试(集成测试)占 20%,大型(端到端)测试占 10%。

单元测试分为:

  • 本地单元测试,始终在JVM驱动的开发计算机上运行的测试,使用robolectric框架。Robolectric 支持Android 的以下几个方面

    • 组件生命周期
    • 事件循环
    • 所有资源
  • 插桩单元测试,在物理设备或者模拟设备上面运行测试,使用AndroidX Test API

    • 主线程,也称为”界面线程“或”Activity 线程“。
    • 插桩线程,大多数测试都在此线程上运行。

1. 本地单元测试

单元测试编写目录对应test目录

1.1 Java 中的 JUnit 测试框架

AndroidStudio 新建项目会自动配置 JUnit 测试框架

1
2
3
4
5
dependencies {
...
testImplementation 'junit:junit:4.12'
...
}

简单的测试案例:

1
2
3
4
5
6
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

JUnit 中的常用注解:

  • @Before:执行每个@Test方法之前调用
  • @After:执行每个@Test方法之后调用
  • @Test:
  • @BeforeClass:在执行一个测试类的所有测试方法之前
  • @AfterClass:在执行一个测试类的所有测试方法之后
  • @Ignore:测试类中某些测试方法可能暂时处于不可测试阶段,那么为了保证这个测试类的正常运行,可以使用@Ignore标记这些测试方法。

1.2 单元测试框架Robolectric

Robolectric 的设计思想是通过 JVM 运行Android代码,从而实现脱离Android环境进行测试。Robolectric 框架依赖于 JUnit,所以需要同时添加两个框架依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
android {
// ...

testOptions {
unitTests {
// 设置单元测试可以访问编译版本的资源
includeAndroidResources = true
}
}
}
dependencies {
...
testImplementation 'junit:junit:4.12'
testImplementation 'org.robolectric:robolectric:4.3'
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RunWith(RobolectricTestRunner.class)
public class MainActivityTest {

// 测试Activity标题是否与期望值一致
@Test
public void titleIsCorrect() {
MainActivity activity = Robolectric.setupActivity(MainActivity.class);
assertEquals("TestingSample", activity.getTitle());
}

// 测试生命周期
@Test
public void testLifecycle(){
ActivityController<MainActivity> controller =
Robolectric.buildActivity(MainActivity.class)
.create().start();
Activity activity = controller.get();
TextView textView =activity.findViewById(R.id.login);
assertEquals("onCreate()",textView.getText().toString());
controller.resume();
assertEquals("onResume()",textView.getText().toString());
}
}

1.3 插桩测试(Mockito)

Mockito 环境配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
android {
// ...

testOptions {
unitTests {
// 设置单元测试可以访问编译版本的资源
includeAndroidResources = true
}
}
}
dependencies {
...
testImplementation 'junit:junit:4.12'
testImplementation 'org.robolectric:robolectric:4.3'
testImplementation 'org.mockito:mockito-core:2.19.0'
...
}
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
public class ExampleUnitTest {

@Test
public void mockTest(){

// 创建接口 List 的 mock 对象
List mockedList = Mockito.mock(List.class);

// 使用 mock 对象执行操作
mockedList.add("one");
mockedList.clear();

//验证对应的操作是否执行过
Mockito.verify(mockedList).add("one");
Mockito.verify(mockedList).clear();

LinkedList linkedList = Mockito.mock(LinkedList.class);

// 在mock对象执行某个操作时,插入桩函数
when(linkedList.get(0)).thenReturn("first");
when(linkedList.get(1)).thenReturn(new RuntimeException());

// 打印 first
System.out.println(linkedList.get(0));

// 打印 java.lang.RuntimeException
System.out.println(linkedList.get(1));

// 打印 null
System.out.println(linkedList.get(999));

}
}

2. 界面测试

界面测试编写目录对应androidTest目录。

2.1 单个应用的界面测试(Espresso)

添加Espresso依赖

1
2
3
4
5
dependencies {
...
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
...
}
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
@RunWith(AndroidJUnit4.class)
public class ChangeTextBehaviorTest {

private String stringToBetyped;

// 需要添加依赖:implementation 'androidx.test:rules:1.2.0'
@Rule
public ActivityTestRule<MainActivity> activityTestRule =
new ActivityTestRule<>(MainActivity.class);

@Before
public void initValidString() {
// Specify a valid string.
stringToBetyped = "Espresso";
}

@Test
public void changeText_sameActivity() {
onView(withId(R.id.editTextUserInput)).perform(typeText(stringToBetyped),
ViewActions.closeSoftKeyboard());
onView(withId(R.id.changeTextBt)).perform(click());

onView(withId(R.id.textToBeChanged))
.check(matches(withText(stringToBetyped)));
}
}

2.2 多个应用的界面测试(UIAutomator)

添加UIAutomator依赖:

1
2
3
4
5
dependencies {
...
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
...
}

设计测试案例之前,需要确保UI Anutomator可以访问你的组件,即组件是否具有可见性或是否设置 android:contentDescription 属性。

2.2.1 uiautomatorviewer 工具

使用 uiautomatorviewer 的步骤:

  • 在你模拟器或者测试设备上打开要测试的应用,并连接到AndroidStudio
  • 前往 Android/sdk/tools/bin 目录打开 uiautomatorviewer 工具。
  • 在 uiautomatorviewer 界面选择 device screenshot ,在右侧即可看见组件具体信息。
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
78
@RunWith(AndroidJUnit4.class)
@SdkSuppress(minSdkVersion = 18)
public class UIAutomatorChangeTextBehaviorTest {

private static final String BASIC_SAMPLE_PACKAGE
= "com.markzl.android.testingsample";

private static final int LAUNCH_TIMEOUT = 5000;

private static final String STRING_TO_BE_TYPED = "UiAutomator";

private UiDevice mDevice;

@Before
public void startMainActivityFromHomeScreen() {

mDevice = UiDevice.getInstance(getInstrumentation());
mDevice.pressHome();

final String launcherPackage = getLauncherPackageName();
assertThat(launcherPackage, notNullValue());
mDevice.wait(Until.hasObject(By.pkg(launcherPackage).depth(0)), LAUNCH_TIMEOUT);

// Launch the blueprint app
Context context = getApplicationContext();
final Intent intent = context.getPackageManager()
.getLaunchIntentForPackage(BASIC_SAMPLE_PACKAGE);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
context.startActivity(intent);

mDevice.wait(Until.hasObject(By.pkg(BASIC_SAMPLE_PACKAGE).depth(0)), LAUNCH_TIMEOUT);


}

@Test
public void checkPreconditions(){
assertThat(mDevice,notNullValue());
}

@Test
public void testChangeText_sameActivity(){
mDevice.findObject(By.res(BASIC_SAMPLE_PACKAGE,"editTextUserInput"))
.setText(STRING_TO_BE_TYPED);
mDevice.findObject(By.res(BASIC_SAMPLE_PACKAGE,"changeTextBt"))
.click();

UiObject2 changedText = mDevice.wait(Until.findObject(By.res(BASIC_SAMPLE_PACKAGE,"textToBeChanged")),
500);
assertThat(changedText.getText(),is(equalTo(STRING_TO_BE_TYPED)));
}

@Test
public void testChangeText_newActivity(){
mDevice.findObject(By.res(BASIC_SAMPLE_PACKAGE,"editTextUserInput"))
.setText(STRING_TO_BE_TYPED);
mDevice.findObject(By.res(BASIC_SAMPLE_PACKAGE,"activityChangeTextBtn"))
.click();

UiObject2 changedText = mDevice.wait(Until.findObject(By.res(BASIC_SAMPLE_PACKAGE,"show_text_view")),
500);
assertThat(changedText.getText(),is(equalTo(STRING_TO_BE_TYPED)));

}


private String getLauncherPackageName() {

final Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_HOME);

PackageManager packageManager = getApplicationContext().getPackageManager();
ResolveInfo resolveInfo = packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
return resolveInfo.activityInfo.packageName;

}

}

UIAutomator API:

  • UiCollection
  • UiObject
  • UiScrollable
  • UiSelector
  • Configurator
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×