Android自动化测试总结

测试金字塔

沿着金字塔逐级向上,从小型测试到大型测试,各类测试的保真度逐级提高,但维护和调试工作所需的执行时间和工作量也逐级增加。因此,您编写的单元测试应多于集成测试,集成测试应多于端到端测试。虽然各类测试的比例可能会因应用的用例不同而异,但我们通常建议各类测试所占比例如下:小型测试占70%,中型测试占20%,大型测试占10%

单元测试(小型测试)

用于验证应用的行为,一次验证一个类。

原则(F.I.R.S.T

Fast(快),单元测试要运行的足够快,单个测试方法一般要立即(一秒之内)给出结果。
Idependent(独立),测试方法之间不要有依赖(先执行某个测试方法,再执行另一个测试方法才能通过)。
Repeatable(重复),可以在本地或 CI 不同环境(机器上)上反复执行,不会出现不稳定的情况。
Self-Validating(自验证),单元测试必须包含足够多的断言进行自我验证。
Timely(及时),理想情况下应测试先行,至少保证单元测试应该和实现代码一起及时完成并提交。

除此之外,测试代码应该具备最好的可读性和最少的维护代价,绝大多数情况下写测试应该就像用领域特定语言描述一个事实,甚至不用经过仔细地思考

构建本地单元测试

当需要更快地运行测试而不需要与在真实设备上运行测试关联的保真度和置信度时,可以使用本地单元测试来验证应用的逻辑。

  • 如果测试对Android框架有依赖性(特别是与框架建立复杂交互的测试),则最好使用 Robolectric添加框架依赖项。

例:待测试的类同时依赖ContextIntentBundleApplicationAndroid Framework中的类时,此时我们可以引入Robolectric框架进行本地单元测试的编写。

  • 如果测试对Android框架的依赖性极小,或者如果测试仅取决于我们自己应用的对象,则可以使用诸如Mockito之类的模拟框架添加模拟依赖项。(BasicUnitAndroidTest)

例:待测试的类只依赖java api(最理想的情况),此时对于待测试类所依赖的其他类我们就可以利用Mockito框架mock其依赖类,再进行当前类的单元测试编写。(EmailValidatorTest)

例:待测试的类除了依赖java api外仅依赖Android FrameworkContext这个类,此时我们就可以利用Mockito框架mock Context类,再进行当前类的单元测试编写。(SharedPreferencesHelperTest)

设置测试环境

Android Studio项目中,本地单元测试的源文件存储在module-name/src/test/java/中。

在模块的顶级build.gradle文件中,将以下库指定为依赖项:

1
2
3
4
5
6
7
8
9
10
11
dependencies {
// Required -- JUnit 4 framework
testImplementation "junit:junit:$junitVersion"
// Optional -- Mockito framework
testImplementation "org.mockito:mockito-core:$mockitoCoreVersion"

// Optional -- Robolectric environment
testImplementation "androidx.test:core:$xcoreVersion"
testImplementation "androidx.test.ext:junit:$extJunitVersion"
testImplementation "org.robolectric:robolectric:$robolectricVersion"
}

如果单元测试依赖于资源,需要在module的build.gradle文件中启用includeAndroidResources选项。然后,单元测试可以访问编译版本的资源,从而使测试更快速且更准确地运行。

1
2
3
4
5
6
7
8
9
android {
// ...

testOptions {
unitTests {
includeAndroidResources = true
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RunWith(AndroidJUnit4::class)
@Config(manifest = Config.NONE)
class PeopleDaoTest {
private lateinit var database: PeopleDatabase

private lateinit var peopleDao: PeopleDao

@Before
fun `create db`() {
database = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
PeopleDatabase::class.java
).allowMainThreadQueries().build()

peopleDao = database.peopleDao()
}

@Test
fun `should return empty list when getPeople without inserted data`() {
val result = peopleDao.getPeople(pageId = 1)

assertThat(result).isNotNull()
assertThat(result).isEmpty()
}

如果单元测试包含异步操作时,可以使用awaitility库进行测试;当使用RxJava响应式编程库时,可以自定义rule:

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
class RxJavaRule : TestWatcher() {
override fun starting(description: Description?) {
super.starting(description)

RxJavaPlugins.setIoSchedulerHandler {
Schedulers.trampoline()
}
RxJavaPlugins.setNewThreadSchedulerHandler {
Schedulers.trampoline()
}
RxJavaPlugins.setComputationSchedulerHandler {
Schedulers.trampoline()
}

RxAndroidPlugins.setMainThreadSchedulerHandler {
Schedulers.trampoline()
}
RxAndroidPlugins.setInitMainThreadSchedulerHandler {
Schedulers.trampoline()
}
}

override fun finished(description: Description?) {
super.finished(description)

RxJavaPlugins.reset()

RxAndroidPlugins.reset()
}
}

TestSchedulertriggerActions的使用。

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
@RunWith(JUnit4::class)
class FilmViewModelTest {
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
@get:Rule
val rxJavaRule = RxJavaRule()

private val repository = mock(Repository::class.java)

private val testScheduler = TestScheduler()

private lateinit var viewModel: FilmViewModel

@Before
fun init() {
viewModel = FilmViewModel(repository)
}

@Test
fun `should return true when loadFilms is loading`() {
`when`(repository.getPopularFilms(1)).thenReturn(
Single.just(emptyList<Film>())
.subscribeOn(testScheduler)
)

viewModel.loadFilms(0)

assertThat(getValue(viewModel.isLoading)).isTrue()
testScheduler.triggerActions()
assertThat(getValue(viewModel.isLoading)).isFalse()
}

@Test
fun `should return films list when loadFilms successful`() {
`when`(repository.getPopularFilms(1)).thenReturn(
Single.just(
listOf(
Film(123, "", "", "", "", "", "", 1)
)
).subscribeOn(testScheduler)
)

viewModel.loadFilms(0)

assertThat(getValue(viewModel.films)).isNull()
testScheduler.triggerActions()
assertThat(getValue(viewModel.films)).isNotNull()
assertThat(getValue(viewModel.films).size).isEqualTo(1)
}
}

TestSubscriber的使用。

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
@RunWith(JUnit4::class)
class WebServiceTest {
private lateinit var webService: WebService

private lateinit var mockWebServer: MockWebServer

@get:Rule
val instantExecutorRule = InstantTaskExecutorRule()

@Before
fun `start service`() {
mockWebServer = MockWebServer()

webService = Retrofit.Builder()
.baseUrl(mockWebServer.url("/"))
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build()
.create(WebService::class.java)
}

@Test
fun `should return fim list when getFilms successful`() {
assertThat(webService).isNotNull()

enqueueResponse("popular_films.json")

val testObserver = webService.getPopularFilms(page = 1)
.map {
it.data
}.test()

testObserver.assertNoErrors()
testObserver.assertValueCount(1)
testObserver.assertValue {
assertThat(it).isNotEmpty()
assertThat(it[0].id).isEqualTo(297761)
assertThat(it[1].id).isEqualTo(324668)
it.size == 2
}
testObserver.assertComplete()
testObserver.dispose()
}

@After
fun `stop service`() {
mockWebServer.shutdown()
}

private fun enqueueResponse(fileName: String) {
val inputStream = javaClass.classLoader?.getResourceAsStream("api-response/$fileName")
?: return
val source = inputStream.source().buffer()
val mockResponse = MockResponse()
mockWebServer.enqueue(
mockResponse
.setBody(source.readString(Charsets.UTF_8))
)
}
}

构建插桩单元测试

插桩单元测试是在物理设备和模拟器上运行的测试,此类测试可以利用Android框架API。插桩测试提供的保真度比本地单元测试要高,但运行速度要慢得多。因此,我们建议只有在必须针对真实设备的行为进行测试时才使用插桩单元测试。

设置测试环境

Android Studio项目中,插桩测试的源文件存储在module-name/src/androidTest/java/

在模块的顶级build.gradle文件中,将以下库指定为依赖项:

1
2
3
4
5
android {
defaultConfig {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
}
1
2
3
4
5
6
7
8
9
dependencies {
androidTestImplementation "androidx.test.ext:junit:$extJunitVersion"
androidTestImplementation "androidx.test:core:$xcoreVersion"
androidTestImplementation "androidx.test:rules:$rulesVersion"
// Optional -- Truth library
androidTestImplementation "androidx.test.ext:truth:$androidxtruthVersion"
androidTestImplementation "org.mockito:mockito-core:$mockitoCoreVersion"
androidTestImplementation "org.mockito:mockito-android:$mockitoAndroidVersion"
}
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
@RunWith(AndroidJUnit4::class)
@SmallTest
class FilmDaoTest {
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()

private lateinit var database: FilmDatabase

private lateinit var filmDao: FilmDao

@Before
fun initDb() {
database = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
FilmDatabase::class.java
).build()

filmDao = database.filmData()
}

@Test
fun should_return_film_list_when_getFilms_with_inserted_film_list() {
filmDao.insert(
Film(100, "", "", "", "", "", "", 1)
)
filmDao.insert(
Film(101, "", "", "", "", "", "", 1)
)

val result = filmDao.getFilms(1)

assertThat(result).isNotNull()
assertThat(result).isNotEmpty()
assertThat(result.size).isEqualTo(2)
assertThat(result[0].id).isEqualTo(100)
assertThat(result[0].page).isEqualTo(1)
assertThat(result[1].id).isEqualTo(101)
}

@Test
fun should_return_film_list_with_size_1_when_getFilms_with_inserted_2_same_film() {
filmDao.insert(
Film(100, "", "", "", "", "", "", 1)
)
filmDao.insert(
Film(100, "1223", "111", "", "", "", "", 1)
)

val result = filmDao.getFilms(1)

assertThat(result).isNotNull()
assertThat(result).isNotEmpty()
assertThat(result.size).isEqualTo(1)
assertThat(result[0].id).isEqualTo(100)
assertThat(result[0].page).isEqualTo(1)
}

@Test
fun should_return_empty_list_when_getFilms_with_deleteAll_called() {
filmDao.insert(
Film(100, "", "", "", "", "", "", 1)
)
filmDao.deleteAll()

val newResult = filmDao.getFilms(1)

assertThat(newResult).isNotNull()
assertThat(newResult).isEmpty()
}

@After
fun closeDb() = database.close()
}

总结

  • 基于目前流行的MVPMVVM架构设计模式,MVPModel层和Presenter层尽量不依赖Android FrameworkMVVMModel层和ViewModel层尽量不依赖Android Framework

  • 类的设计做到单一职责原则,依赖其他类时提供方便mock的方式(例如作为构造方法参数传递),某一个方法依赖其他对象时,小重构该对象作为方法参数传入。

  • 方法尽量短小(方法太长时可以利用重构手法在方法中再提取方法)。

  • 只覆盖public方法单元测试,privite方法可以间接测试。

  • 当依赖Android Framework API非常少时,可以采用Mock Android api的方式。

  • 当严重依赖Android Framework API时,引入Robolectric库模拟Android环境或者放入AndroidTest目录作为插桩单元测试在物理设备上跑。

  • 使用Robolectric库写本地单元测试时,依赖的某些类的方法调用出问题导致测试failed时,可以使用shadow类提供默认实现。

  • 每条测试采用GivenWhenThen的方式进行区分.

    1
    2
    3
    4
    5
    6
    7
    8
    @Test
    public void should_do_something_if_some_condition_fulfills() {
    // Given 设置前置条件

    // When 执行被测方法

    // Then 验证方法结果
    }

集成测试(中型测试)

用于验证模块内堆栈级别之间的交互或相关模块之间的交互

  • 如果应用使用了用户不直接与之交互的组件(如ServiceContentProvider),应验证这些组件在应用中的行为是否正确。

设置测试环境

参考插桩单元测试环境设置

Service测试

  • 利用ServiceTestRule,可在单元测试方法运行之前启动服务,并在测试完成后关闭服务。
  • ServiceTestRule类不支持测试IntentService对象。如果需要测试IntentService对象,可以应将逻辑封装在一个单独的类中,并创建相应的单元测试。
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
@MediumTest
@RunWith(AndroidJUnit4.class)
public class LocalServiceTest {
@Rule
public final ServiceTestRule mServiceRule = new ServiceTestRule();

@Test
public void testWithBoundService() throws TimeoutException {
// Create the service Intent.
Intent serviceIntent =
new Intent(getApplicationContext(), LocalService.class);

// Data can be passed to the service via the Intent.
serviceIntent.putExtra(LocalService.SEED_KEY, 42L);

// Bind the service and grab a reference to the binder.
IBinder binder = mServiceRule.bindService(serviceIntent);

// Get the reference to the service, or you can call public methods on the binder directly.
LocalService service = ((LocalService.LocalBinder) binder).getService();

// Verify that the service is working correctly.
assertThat(service.getRandomInt(), is(any(Integer.class)));
}
}

ContentProvider的测试

使用ProviderTestRule

1
2
3
4
5
6
7
8
9
10
11
12
@Rule
public ProviderTestRule mProviderRule =
new ProviderTestRule.Builder(MyContentProvider.class, MyContentProvider.AUTHORITY).build();

@Test
public void verifyContentProviderContractWorks() {
ContentResolver resolver = mProviderRule.getResolver();
// perform some database (or other) operations
Uri uri = resolver.insert(testUrl, testContentValues);
// perform some assertions on the resulting URI
assertNotNull(uri);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Rule
public ProviderTestRule mProviderRule =
new ProviderTestRule.Builder(MyContentProvider.class, MyContentProvider.AUTHORITY)
.setDatabaseCommands(DATABASE_NAME, INSERT_ONE_ENTRY_CMD, INSERT_ANOTHER_ENTRY_CMD)
.build();

@Test
public void verifyTwoEntriesInserted() {
ContentResolver mResolver = mProviderRule.getResolver();
// two entries are already inserted by rule, we can directly perform assertions to verify
Cursor c = null;
try {
c = mResolver.query(URI_TO_QUERY_ALL, null, null, null, null);
assertNotNull(c);
assertEquals(2, c.getCount());
} finally {
if (c != null && !c.isClosed()) {
c.close();
}
}
}
  • Android没有为BroadcastReceiver提供单独的测试用例类。要验证 BroadcastReceiver是否正确响应,可以测试向其发送Intent对象的组件。或者,可以通过调用ApplicationProvider.getApplicationContext()来创建BroadcastReceiver的实例,然后调用要测试的BroadcastReceiver方法(通常是onReceive()方法)。

端到端测试(大型测试)

用于验证跨越了应用的多个模块的用户操作流程

界面测试的一种方法是直接让测试人员对目标应用执行一系列用户操作,并验证其行为是否正常。不过,这种人工方法会非常耗时、繁琐且容易出错。一种更高效的方法是编写界面测试,以便以自动化方式执行用户操作。自动化方法可以以可重复的方式快速可靠地运行测试。

设置测试环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
dependencies {
androidTestImplementation "androidx.test.ext:junit:$extJunitVersion"
androidTestImplementation "androidx.test:core:$xcoreVersion"
androidTestImplementation "androidx.test:rules:$rulesVersion"
// Optional -- Truth library
androidTestImplementation "androidx.test.ext:truth:$androidxtruthVersion"
androidTestImplementation "org.mockito:mockito-core:$mockitoCoreVersion"
androidTestImplementation "org.mockito:mockito-android:$mockitoAndroidVersion"
// Optional -- UI testing with Espresso
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
androidTestImplementation "androidx.test.espresso:espresso-intents:$espressoVersion"
// Optional -- UI testing with UI Automator
androidTestImplementation "androidx.test.uiautomator:uiautomator:$uiautomatorVersion"
}
  • 涵盖单个应用的界面测试:这种类型的测试可验证目标应用在用户执行特定操作或在其 Activity 中输入特定内容时的行为是否符合预期。它可让您检查目标应用是否返回正确的界面输出来响应应用 Activity 中的用户交互。诸如 Espresso 之类的界面测试框架可让您以编程方式模拟用户操作,并测试复杂的应用内用户交互。(espresso测试单个应用的界面例子)

  • 涵盖多个应用的界面测试:这种类型的测试可验证不同用户应用之间交互或用户应用与系统应用之间交互的正确行为。例如,您可能想要测试相机应用是否能够与第三方社交媒体应用或默认的 Android 相册应用正确分享图片。支持跨应用交互的界面测试框架(如 UI Automator)可让您针对此类场景创建测试。(uiautomator测试多个应用的界面

参考例子testing-samples