一、测试基础知识
1.1 测试级别
测试金字塔(如图 2 所示)说明了应用应如何包含三类测试(即小型、中型和大型测试):
-
小型测试是指单元测试,用于验证应用的行为,一次验证一个类。
-
中型测试是指集成测试,用于验证模块内堆栈级别之间的互动或相关模块之间的互动。
-
大型测试是指端到端测试,用于验证跨越了应用的多个模块的用户操作流程。
沿着金字塔逐级向上,从小型测试到大型测试,各类测试的保真度逐级提高,但维护和调试工作所需的执行时间和工作量也逐级增加。因此,您编写的单元测试应多于集成测试,集成测试应多于端到端测试。虽然各类测试的比例可能会因应用的用例不同而异,但我们通常建议各类测试所占比例如下:小型测试占 70%,中型测试占 20%,大型测试占 10%。
图1 测试金字塔,显示了应用的测试套件应包含的三类测试
1.2 迭代创建和测试代码
迭代开发某项功能时,您可以先编写一个新测试,也可以将用例和断言添加到现有单元测试。测试最初会失败,因为该功能尚未实现。
务必考虑随着设计新功能而出现的责任单元。对于每个单元,您需要编写相应的单元测试。您的单元测试应几乎囊括与单元的所有可能的互动,包括标准互动、无效输入以及资源不可用的情况。应尽可能利用 Jetpack 库;当您使用这些经过充分测试的库时,您可以专注于验证您的应用特有的行为。
图2 与由测试驱动的迭代开发关联的两个周期
完整的工作流(如图 1 所示)包含一系列嵌套的迭代周期,其中一个由界面驱动的漫长而缓慢的周期用来测试代码单元的集成。您可以使用更短且更快的开发周期来测试单元本身。这一组周期一直持续到您的应用满足每个用例为止。
二、构建单元测试
单元测试是应用测试策略中的基本测试。通过针对代码创建和运行单元测试,您可以轻松验证各个单元的逻辑是否正确。在每次构建后运行单元测试可帮助您快速捕捉和修复由应用的代码更改导致的软件回归。
单元测试通常以可重复的方式运用尽可能小的代码单元(可能是方法、类或组件)的功能。当您需要验证应用中特定代码的逻辑时,应构建单元测试。例如,如果您正在对某个类进行单元测试,测试可能会检查该类是否处于正确状态。通常,代码单元在隔离的环境中进行测试;您的测试仅影响和监控对该单元的更改。您可以使用依赖项提供器(如 Robolectric)或模拟框架将您的单元与其依赖项隔离开来。
2.1 构建本地单元测试
设置测试环境
在 Android Studio 项目中,您必须将本地单元测试的源文件存储在 module-name/src/test/java/
中。当您创建新项目时,此目录已存在。
您还需要为项目配置测试依赖项,以使用 JUnit 4 框架提供的标准 API。如果您的测试需要与 Android 依赖项互动,请添加 Robolectric 或 Mockito 库以简化您的本地单元测试。
在应用的顶级 build.gradle
文件中,请将以下库指定为依赖项:
dependencies { // Required -- JUnit 4 framework testImplementation 'junit:junit:4.12' // Optional -- Robolectric environment testImplementation 'androidx.test:core:1.0.0' // Optional -- Mockito framework testImplementation 'org.mockito:mockito-core:1.10.19' }
创建本地单元测试类
本地单元测试类应编写为 JUnit 4 测试类。JUnit 是最受欢迎且应用最广泛的 Java 单元测试框架。与原先的版本相比,JUnit 4 可让您以更简洁且更灵活的方式编写测试,因为 JUnit 4 不要求您执行以下操作:
-
扩展
junit.framework.TestCase
类。 -
在测试方法名称前面加上
'test'
关键字作为前缀。 -
使用
junit.framework
或junit.extensions
软件包中的类。
如需创建基本的 JUnit 4 测试类,请创建包含一个或多个测试方法的类。测试方法以 @Test
注释开头,并且包含用于运用和验证要测试的组件中的单项功能的代码。
以下示例展示了如何实现本地单元测试类。测试方法 emailValidator_CorrectEmailSimple_ReturnsTrue
验证被测应用中的 isValidEmail()
方法是否返回正确的结果。
import com.google.common.truth.Truth.assertThat import org.junit.Test class EmailValidatorTest { @Test fun emailValidator_CorrectEmailSimple_ReturnsTrue() { assertThat(EmailValidator.isValidEmail("name@email.com")).isTrue() } }
如需创建容易读懂的测试来评估应用中的组件是否返回预期的结果,我们建议使用 Truth 库和 Android Assertions 中的类,如前面的示例所示。如需详细了解 Truth 和 Android Assertions 支持哪些类型的逻辑验证,请参阅介绍如何创建更容易读懂的断言的部分。
不过,如果您更愿意使用 junit.Assert 方法或 Hamcrest 匹配器(如 is()
和 equalTo()
方法)来比较预期结果与实际结果,也可以改用这些库。
添加框架依赖项
如果您的测试与多个 Android 框架依赖项互动,或以复杂的方式与这些依赖项互动,请使用 AndroidX Test 提供的 Robolectric 工件。Robolectric 在本地 JVM 或真实设备上执行真实的 Android 框架代码和原生框架代码的虚假对象。
以下示例展示了如何创建使用 Robolectric 的单元测试:
app/build.gradle
android { // ... testOptions { unitTests.includeAndroidResources = true } }
MyLocalUnitTestClass
import android.content.Context import androidx.test.core.app.ApplicationProvider import com.google.common.truth.Truth.assertThat import org.junit.Test private const val FAKE_STRING = "HELLO_WORLD" class UnitTestSample { val context = ApplicationProvider.getApplicationContext<Context>() @Test fun readStringFromContext_LocalizedString() { // Given a Context object retrieved from Robolectric... val myObjectUnderTest = ClassUnderTest(context) // ...when the string is returned from the object under test... val result: String = myObjectUnderTest.getHelloWorldString() // ...then the result should be the expected one. assertThat(result).isEqualTo(FAKE_STRING) } }
添加模拟依赖项
默认情况下,Android Plug-in for Gradle 针对一个修改版 android.jar
库(不包含任何实际代码)执行本地单元测试。从单元测试对 Android 类的方法调用会抛出异常。这是为了确保您仅测试代码,而不依赖于 Android 平台的任何特定行为,即您未明确构建或模拟的行为。
模拟 Android 依赖项
如果您的测试对 Android 的依赖性极小,并且您需要在应用中测试组件与其依赖项之间的特定互动,请使用模拟框架对代码中的外部依赖项打桩。这样,您就可以轻松地测试组件是否按预期方式与依赖项互动。通过用模拟对象代替 Android 依赖项,您可以将单元测试与 Android 系统的其余部分隔离,同时验证是否调用了这些依赖项中的正确方法。适用于 Java(版本 1.9.5 及更高版本)的 Mockito 模拟框架提供了与 Android 单元测试的兼容性。通过 Mockito,您可以将模拟对象配置为在被调用时返回某个特定值。
如需使用此框架将模拟对象添加到本地单元测试,请遵循以下编程模型:
-
在
build.gradle
文件中添加 Mockito 库依赖项,如设置测试环境中所述。 -
在单元测试类定义的开头,添加
@RunWith(MockitoJUnitRunner.class)
注释。此注释可告知 Mockito 测试运行程序验证您对框架的使用是否正确无误,并简化了模拟对象的初始化。 -
如需为 Android 依赖项创建模拟对象,请在字段声明前添加
@Mock
注释。 -
如需模拟依赖项的行为,您可以使用
when()
和thenReturn()
方法来指定某种条件以及满足该条件时的返回值。
以下示例展示了如何创建使用模拟 Context
对象的单元测试。
import android.content.Context import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.`when` import org.mockito.junit.MockitoJUnitRunner private const val FAKE_STRING = "HELLO WORLD" @RunWith(MockitoJUnitRunner::class) class UnitTestSample { @Mock private lateinit var mockContext: Context @Test fun readStringFromContext_LocalizedString() { // Given a mocked Context injected into the object under test... `when`(mockContext.getString(R.string.hello_word)) .thenReturn(FAKE_STRING) val myObjectUnderTest = ClassUnderTest(mockContext) // ...when the string is returned from the object under test... val result: String = myObjectUnderTest.getHelloWorldString() // ...then the result should be the expected one. assertThat(result, `is`(FAKE_STRING)) } }
错误:“Method ... not mocked”
如果您运行的测试从并未模拟的 Android SDK 调用 API,您会收到一条错误,指出未模拟此方法。这是因为,用于运行单元测试的 android.jar
文件不包含任何实际代码(这些 API 仅由设备上的 Android 系统映像提供)。
默认情况下,所有方法都会抛出异常。这是为了确保单元测试仅测试代码,而不依赖于 Android 平台的任何特定行为,即您未明确模拟(如使用 Mockito 模拟)的行为。
如果抛出的异常会给测试带来问题,您可以通过在项目的顶级 build.gradle
文件中添加以下配置来更改行为,以使方法返回 null 或 0:
android { ... testOptions { unitTests.returnDefaultValues = true } }
2.2 构建插桩单元测试
插桩单元测试是在实体设备和模拟器上运行的测试,此类测试可以利用 Android 框架 API 和辅助性 API,如 AndroidX Test。插桩测试提供的保真度比本地单元测试要高,但运行速度要慢得多。因此,我们建议只有在必须针对真实设备的行为进行测试时才使用插桩单元测试。AndroidX Test 提供了几个库,可让您在必要时更轻松地编写插桩单元测试。例如,Android Builder 类可让您更轻松地创建本来难以构建的 Android 数据对象。
设置测试环境
在 Android Studio 项目中,您必须将插桩测试的源文件存储在 module-name/src/androidTest/java/
中。此目录在您创建新项目时已存在,并且包含一个插桩测试示例。
在开始之前,您应先添加 AndroidX Test API,以便为您的应用快速构建和运行插桩测试代码。AndroidX Test 包含 JUnit 4 测试运行程序 (AndroidJUnitRunner
) 和用于功能界面测试的 API(Espresso 和 UI Automator)。
您还需要为项目配置 Android 测试依赖项,以使用 AndroidX Test 提供的测试运行程序和规则 API。为了简化测试开发,您还应添加 Hamcrest 库,该库可让您使用 Hamcrest 匹配器 API 创建更灵活的断言。
在应用的顶级 build.gradle
文件中,您需要将以下库指定为依赖项:
dependencies { androidTestImplementation 'androidx.test:runner:1.1.0' androidTestImplementation 'androidx.test:rules:1.1.0' // Optional -- Hamcrest library androidTestImplementation 'org.hamcrest:hamcrest-library:1.3' // Optional -- UI testing with Espresso androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' // Optional -- UI testing with UI Automator androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0' }
如需使用 JUnit 4 测试类,请务必在您的项目中将 AndroidJUnitRunner
指定为默认插桩测试运行程序,方法是在应用的模块级 build.gradle
文件中添加以下设置:
android { defaultConfig { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } }
创建插桩单元测试类
插桩单元测试类应该是一个 JUnit 4 测试类,它类似于有关如何创建本地单元测试类的部分中介绍的类。
如需创建 JUnit 4 插桩测试类,请将 AndroidJUnit4
指定为默认测试运行程序。
注意:如果您的测试套件依赖于 JUnit3 和 JUnit4 库的混合搭配,请在测试类定义的开头添加 @RunWith(AndroidJUnit4::class)
注释。
以下示例展示了如何编写插桩单元测试来验证是否为 LogHistory
类正确实现了 Parcelable
接口:
import android.os.Parcel import android.text.TextUtils.writeToParcel import androidx.test.filters.SmallTest import androidx.test.runner.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith const val TEST_STRING = "This is a string" const val TEST_LONG = 12345678L // @RunWith is required only if you use a mix of JUnit3 and JUnit4. @RunWith(AndroidJUnit4::class) @SmallTest class LogHistoryAndroidUnitTest { private lateinit var logHistory: LogHistory @Before fun createLogHistory() { logHistory = LogHistory() } @Test fun logHistory_ParcelableWriteRead() { val parcel = Parcel.obtain() logHistory.apply { // Set up the Parcelable object to send and receive. addEntry(TEST_STRING, TEST_LONG) // Write the data. writeToParcel(parcel, describeContents()) } // After you're done with writing, you need to reset the parcel for reading. parcel.setDataPosition(0) // Read the data. val createdFromParcel: LogHistory = LogHistory.CREATOR.createFromParcel(parcel) createdFromParcel.getData().also { createdFromParcelData: List<Pair<String, Long>> -> // Verify that the received data is correct. assertThat(createdFromParcelData.size).isEqualTo(1) assertThat(createdFromParcelData[0].first).isEqualTo(TEST_STRING) assertThat(createdFromParcelData[0].second).isEqualTo(TEST_LONG) } } }
创建测试套件
为了使插桩单元测试的执行有条不紊,您可以将一系列测试类归入一个测试套件类,并将这些测试一起运行。测试套件可以嵌套;您的测试套件可以将其他测试套件归在一起,并将其所有组件测试类一起运行。
测试套件包含在测试软件包中,类似于主应用软件包。按照惯例,测试套件软件包名称通常以 .suite
后缀结尾(例如,com.example.android.testing.mysample.suite
)。
如需为您的单元测试创建测试套件,请导入 JUnit RunWith
和 Suite
类。在您的测试套件中,添加 @RunWith(Suite.class)
和 @Suite.SuitClasses()
注释。在 @Suite.SuiteClasses()
注释中,将各个测试类或测试套件作为参数列出。
以下示例展示了如何实现名为UnitTestSuite
的测试套件,该测试套件将CalculatorInstrumentationTest
和CalculatorAddParameterizedTest
测试类组合在一起并运行。
import com.example.android.testing.mysample.CalculatorAddParameterizedTest import com.example.android.testing.mysample.CalculatorInstrumentationTest import org.junit.runner.RunWith import org.junit.runners.Suite // Runs all unit tests. @RunWith(Suite::class) @Suite.SuiteClasses(CalculatorInstrumentationTest::class, CalculatorAddParameterizedTest::class) class UnitTestSuite
利用 Firebase 测试实验室运行测试
使用 Firebase 测试实验室,您可以同时在许多主流 Android 设备和设备配置(语言区域、屏幕方向、屏幕尺寸和平台版本)上测试您的应用。这些测试在远程 Google 数据中心的物理设备和虚拟设备上运行。您可以直接通过 Android Studio 或从命令行将应用部署到测试实验室。测试结果会提供测试日志,并包含所有应用故障的详细信息。
在开始使用 Firebase 测试实验室之前,除非您已经拥有 Google 帐号和 Firebase 项目,否则您需要执行以下操作:
-
创建一个 Google 帐号(如果您还没有帐号)。
-
在 Firebase 控制台中,点击新建项目。
-
利用测试实验室在 Spark 方案每日免费配额内测试您的应用时不收取任何费用。
三、自动执行界面测试
通过界面测试,您可以确保应用满足其功能要求并达到较高的质量标准,从而更有可能成功地被用户采用。
界面测试的一种方法是直接让测试人员对目标应用执行一系列用户操作,并验证其行为是否正常。不过,这种人工方法会非常耗时、繁琐且容易出错。一种更高效的方法是编写界面测试,以便以自动化方式执行用户操作。自动化方法可让您以可重复的方式快速可靠地运行测试。
如需使用 Android Studio 自动执行界面测试,请在单独的 Android 测试文件夹 (src/androidTest/java
) 中实现测试代码。Android Plugin for Gradle 会根据测试代码构建一个测试应用,然后在目标应用所在的设备上加载该测试应用。在测试代码中,您可以使用界面测试框架来模拟目标应用上的用户交互,以便执行涵盖特定使用场景的测试任务。
为了测试 Android 应用,您通常会创建下面这些类型的自动化界面测试:
-
涵盖单个应用的界面测试:这种类型的测试可验证目标应用在用户执行特定操作或在其 Activity 中输入特定内容时的行为是否符合预期。它可让您检查目标应用是否返回正确的界面输出来响应应用 Activity 中的用户交互。诸如 Espresso 之类的界面测试框架可让您以编程方式模拟用户操作,并测试复杂的应用内用户交互。
-
涵盖多个应用的界面测试:这种类型的测试可验证不同用户应用之间交互或用户应用与系统应用之间交互的正确行为。例如,您可能想要测试相机应用是否能够与第三方社交媒体应用或默认的 Android 相册应用正确分享图片。支持跨应用交互的界面测试框架(如 UI Automator)可让您针对此类场景创建测试。
测试单个应用的界面
测试单个应用内的用户交互有助于确保用户在与应用交互时不会遇到意外结果或体验不佳的情况。如果您需要验证应用的界面是否正常运行,应养成创建界面测试的习惯。
由 AndroidX Test 提供的 Espresso 测试框架提供了一些 API,用于编写界面测试以模拟单个目标应用内的用户交互。Espresso 测试可以在搭载 Android 2.3.3(API 级别 10)及更高版本的设备上运行。使用 Espresso 的主要好处在于,它可以自动同步测试操作与您正在测试的应用的界面。Espresso 会检测主线程何时处于空闲状态,以便可以在适当的时间运行测试命令,从而提高测试的可靠性。此外,借助该功能,您不必在测试代码中添加任何计时解决方法,如 Thread.sleep()
。
Espresso 测试框架是基于插桩的 API,可与 AndroidJUnitRunner
测试运行程序一起使用
设置 Espresso
在使用 Espresso 构建界面测试之前,请务必设置对 Espresso 库的依赖项引用:
dependencies { androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' }
在测试设备上关闭动画 - 如果让系统动画在测试设备上保持开启状态,可能会导致意外结果或导致测试失败。通过以下方式关闭动画:在“设置”中打开“开发者选项”,然后关闭以下所有选项:
-
窗口动画缩放
-
过渡动画缩放
-
Animator 时长缩放
创建 Espresso 测试类、
如需创建 Espresso 测试,请遵循以下编程模型:
-
通过调用
onView()
方法或AdapterView
控件的onData()
方法,在Activity
中找到要测试的界面组件(例如,应用中的登录按钮)。 -
通过调用
ViewInteraction.perform()
或DataInteraction.perform()
方法并传入用户操作(例如,点击登录按钮),模拟要在该界面组件上执行的特定用户交互。如需对同一界面组件上的多项操作进行排序,请在方法参数中使用逗号分隔列表将它们链接起来。 -
根据需要重复上述步骤,以模拟目标应用中跨多个 Activity 的用户流。
-
执行这些用户交互后,使用
ViewAssertions
方法检查界面是否反映了预期的状态或行为。
下面几部分更详细地介绍了这些步骤。
以下代码段展示了测试类如何调用此基本工作流程:
onView(withId(R.id.my_view)) // withId(R.id.my_view) is a ViewMatcher .perform(click()) // click() is a ViewAction .check(matches(isDisplayed())) // matches(isDisplayed()) is a ViewAssertion
将 Espresso 与 ActivityTestRule 一起使用
下文介绍如何创建新的 JUnit 4 型 Espresso 测试,并使用 ActivityTestRule
减少您需要编写的样板代码量。通过使用 ActivityTestRule
,测试框架会在带有 @Test
注释的每个测试方法运行之前以及带有 @Before
注释的所有方法运行之前启动被测 Activity。该框架将在测试完成并且带有 @After
注释的所有方法都运行后关闭该 Activity。
package com.example.android.testing.espresso.BasicSample import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import androidx.test.rule.ActivityTestRule import androidx.test.runner.AndroidJUnit4 @RunWith(AndroidJUnit4::class) @LargeTest class ChangeTextBehaviorTest { private lateinit var stringToBetyped: String @get:Rule var activityRule: ActivityTestRule<MainActivity> = ActivityTestRule(MainActivity::class.java) @Before fun initValidString() { // Specify a valid string. stringToBetyped = "Espresso" } @Test fun changeText_sameActivity() { // Type text and then press the button. onView(withId(R.id.editTextUserInput)) .perform(typeText(stringToBetyped), closeSoftKeyboard()) onView(withId(R.id.changeTextBt)).perform(click()) // Check that the text was changed. onView(withId(R.id.textToBeChanged)) .check(matches(withText(stringToBetyped))) } }
访问界面组件
您必须先指定界面组件或视图,然后 Espresso 才能与被测应用进行交互。Espresso 支持使用 Hamcrest 匹配器指定应用中的视图和适配器。
如需查看视图,请调用 onView()
方法并传入用于指定目标视图的视图匹配器。指定视图匹配器部分对此进行了更详细的说明。onView()
方法将返回一个 ViewInteraction
对象,该对象允许测试与视图进行交互。但是,如果希望在 RecyclerView
布局中查找视图,调用 onView()
方法可能不起作用。在这种情况下,请按照在 AdapterView 中查找视图中的说明进行操作。
以下代码段展示了如何编写一个先访问 EditText
字段,再输入文本字符串,接着关闭虚拟键盘,然后执行按钮点击操作的测试。
fun testChangeText_sameActivity() { // Type text and then press the button. onView(withId(R.id.editTextUserInput)) .perform(typeText(STRING_TO_BE_TYPED), closeSoftKeyboard()) onView(withId(R.id.changeTextButton)).perform(click()) // Check that the text was changed. ... }
指定视图匹配器
您可以使用以下方法指定视图匹配器:
-
调用
ViewMatchers
类中的方法。例如,如需通过查找视图显示的文本字符串查找视图,您可以调用以下方法:
onView(withText("Sign-in"))
同样,您可以调用 withId()
并提供视图的资源 ID (R.id
),如以下示例所示:
onView(withId(R.id.button_signin))
-
不能保证 Android 资源 ID 是唯一的。如果测试尝试匹配由多个视图使用的某个资源 ID,Espresso 会抛出
AmbiguousViewMatcherException
。 -
使用
Matchers
类。您可以使用allOf()
方法组合多个匹配器,例如containsString()
和instanceOf()
。此方法可让您更精细地过滤匹配结果,如以下示例所示:
onView(allOf(withId(R.id.button_signin), withText("Sign-in")))
您可以使用 not
关键字过滤与匹配器不对应的视图,如以下示例所示:
onView(allOf(withId(R.id.button_signin), not(withText("Sign-out"))))
如需在测试中使用这些方法,请导入 org.hamcrest.Matchers
软件包。如需详细了解 Hamcrest 匹配,请访问 Hamcrest 网站。
如需提高 Espresso 测试的性能,请指定查找目标视图所需的最少匹配信息。例如,如果某个视图可通过其描述性文本进行唯一标识,您无需指定该视图也可从 TextView
实例分配。
在 AdapterView 中查找视图
在 AdapterView
微件中,视图会在运行时由子视图动态填充。如果您要测试的目标视图位于 AdapterView
(例如 ListView
、GridView
或 Spinner
)内,则 onView()
方法可能不起作用,因为只能将一部分视图加载到当前视图层次结构中。
应改为调用 onData()
方法获取 DataInteraction
对象,以访问目标视图元素。Espresso 负责将目标视图元素加载到当前视图层次结构中。Espresso 还负责滚动到目标元素,并将该元素置于焦点上。
注意:onData()
方法不检查您指定的项是否与视图对应。Espresso 仅搜索当前视图层次结构。如果未找到匹配项,该方法会抛出 NoMatchingViewException
。
以下代码段展示了如何结合使用 onData()
方法和 Hamcrest 匹配搜索列表中包含给定字符串的特定行。在本例中,LongListActivity
类包含通过 SimpleAdapter
公开的字符串列表。
onData(allOf(`is`(instanceOf(Map::class.java)), hasEntry(equalTo(LongListActivity.ROW_TEXT), `is`("test input"))))
执行操作
调用 ViewInteraction.perform()
或 DataInteraction.perform()
方法,以模拟界面组件上的用户交互。您必须将一个或多个 ViewAction
对象作为参数传入。Espresso 将按照给定的顺序依次触发每项操作,并在主线程中执行这些操作。
ViewActions
类提供了用于指定常见操作的辅助程序方法的列表。您可以将这些方法用作方便的快捷方式,而不是创建和配置单个 ViewAction
对象。您可以指定以下操作:
-
ViewActions.click()
:点击视图。 -
ViewActions.typeText()
:点击视图并输入指定的字符串。 -
ViewActions.scrollTo()
:滚动到视图。目标视图必须是由ScrollView
派生的子类,并且其android:visibility
属性的值必须为VISIBLE
。对于扩展AdapterView
的视图(例如ListView
),onData()
方法将负责为您滚动。 -
ViewActions.pressKey()
:使用指定的键码执行按键操作。 -
ViewActions.clearText()
:清除目标视图中的文本。
如果目标视图位于 ScrollView
内,请先执行 ViewActions.scrollTo()
操作以在屏幕中显示该视图,然后再继续执行其他操作。如果已显示该视图,则 ViewActions.scrollTo()
操作将不起作用。
使用 Espresso Intent 单独测试 Activity
Espresso Intent 支持对应用发出的 intent 进行验证和打桩。使用 Espresso Intent,您可以通过以下方式单独测试应用、Activity 或服务:拦截传出 intent,对结果进行打桩,然后将其发送回被测组件。
如需开始使用 Espresso Intent 进行测试,您需要将以下代码行添加到应用的 build.gradle 文件中:
dependencies { androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.0' }
如需测试 intent,您需要创建 IntentsTestRule 类(与 ActivityTestRule 类非常相似)的实例。IntentsTestRule 类会在每次测试前初始化 Espresso Intent,终止托管 Activity,并在每次测试后释放 Espresso Intent。
以下代码段中显示的测试类提供了显式 intent 的简单测试。
private const val MESSAGE = "This is a test" private const val PACKAGE_NAME = "com.example.myfirstapp" @RunWith(AndroidJUnit4::class) class SimpleIntentTest { /* Instantiate an IntentsTestRule object. */ @get:Rule var intentsRule: IntentsTestRule<MainActivity> = IntentsTestRule(MainActivity::class.java) @Test fun verifyMessageSentToMessageActivity() { // Types a message into a EditText element. onView(withId(R.id.edit_message)) .perform(typeText(MESSAGE), closeSoftKeyboard()) // Clicks a button to send the message to another // activity through an explicit intent. onView(withId(R.id.send_message)).perform(click()) // Verifies that the DisplayMessageActivity received an intent // with the correct package name and message. intended(allOf( hasComponent(hasShortClassName(".DisplayMessageActivity")), toPackage(PACKAGE_NAME), hasExtra(MainActivity.EXTRA_MESSAGE, MESSAGE))) } }
使用 Espresso Web 测试 WebView
使用 Espresso Web,您可以测试包含在 Activity 中的 WebView
组件。它使用 WebDriver API 检查和控制 WebView
的行为。
如需开始使用 Espresso Web 进行测试,您需要将以下代码行添加到应用的 build.gradle 文件中:
dependencies { androidTestImplementation 'androidx.test.espresso:espresso-web:3.1.0' }
在使用 Espresso Web 创建测试的过程中,当您实例化 ActivityTestRule 对象以测试 Activity 时,需要在 WebView
上启用 JavaScript。在测试中,您可以选择 WebView
中显示的 HTML 元素并模拟用户交互,例如在文本框中输入文本,然后点击某个按钮。完成这些操作后,您可以验证网页上的结果是否与预期结果一致。
以下代码段中,该类测试被测 Activity 中 ID 值为“webview”的 WebView
组件。typeTextInInput_clickButton_SubmitsForm()
测试先选择网页上的一个 <input>
元素,再输入一些文本,然后检查出现在另一个元素中的文本。
private const val MACCHIATO = "Macchiato" private const val DOPPIO = "Doppio" @LargeTest @RunWith(AndroidJUnit4::class) class WebViewActivityTest { @get:Rule val activityRule = object : ActivityTestRule<WebViewActivity>( WebViewActivity::class.java, false, /* Initial touch mode */ false /* launch activity */ ) { override fun afterActivityLaunched() { // Enable JavaScript. onWebView().forceJavascriptEnabled() } } @Test fun typeTextInInput_clickButton_SubmitsForm() { // Lazily launch the Activity with a custom start Intent per test activityRule.launchActivity(withWebFormIntent()) // Selects the WebView in your layout. // If you have multiple WebViews you can also use a // matcher to select a given WebView, onWebView(withId(R.id.web_view)). onWebView() // Find the input element by ID .withElement(findElement(Locator.ID, "text_input")) // Clear previous input .perform(clearElement()) // Enter text into the input element .perform(DriverAtoms.webKeys(MACCHIATO)) // Find the submit button .withElement(findElement(Locator.ID, "submitBtn")) // Simulate a click via JavaScript .perform(webClick()) // Find the response element by ID .withElement(findElement(Locator.ID, "response")) // Verify that the response page contains the entered text .check(webMatches(getText(), containsString(MACCHIATO))) } }
验证结果
调用 ViewInteraction.check()
或 DataInteraction.check()
方法以断言界面中的视图与某种预期状态匹配。您必须将 ViewAssertion
对象作为参数传入。如果断言失败,Espresso 会抛出 AssertionFailedError
。
ViewAssertions
类提供了用于指定常见断言的辅助程序方法的列表。可以使用的断言包括:
-
doesNotExist
:断言当前视图层次结构中没有符合指定条件的视图。 -
matches
:断言当前视图层次结构中存在指定的视图,并且其状态与某个给定的 Hamcrst 匹配器匹配。 -
selectedDescendentsMatch
:断言存在父视图的指定子视图,并且其状态与某个给定的 Hamcrst 匹配器匹配。
以下代码段展示了如何检查界面中显示的文本与先前在 EditText
字段中输入的文本是否具有相同的值。
fun testChangeText_sameActivity() { // Type text and then press the button. ... // Check that the text was changed. onView(withId(R.id.textToBeChanged)) .check(matches(withText(STRING_TO_BE_TYPED))) }
测试多个应用的界面
通过涉及多个应用中的用户交互的界面测试,您可以验证当用户流跨入其他应用或系统界面时,您的应用是否能够正常运行。短信应用就是此类用户流的一个例子,该应用先让用户输入短信,再启动 Android 联系人选择器,以便用户可以选择短信的收件人,然后将控制权返还给原来的应用,以便用户提交短信。
本课介绍如何使用 AndroidX Test 提供的 UI Automator 测试框架来编写此类界面测试。通过 UI Automator API,您可以与设备上的可见元素进行交互,而不管焦点在哪个 Activity
上。您的测试可以使用方便的描述符(如显示在相应组件中的文本或其内容描述)来查找界面组件。UI Automator 测试可以在搭载 Android 4.3(API 级别 18)或更高版本的设备上运行。
UI Automator 测试框架是基于插桩的 API,可与 AndroidJUnitRunner
测试运行程序一起使用。
设置 UI Automator
在使用 UI Automator 构建界面测试之前,请务必配置测试源代码位置和项目依赖项,如针对 AndroidX Test 设置项目中所述。
在 Android 应用模块的 build.gradle
文件中,您必须设置对 UI Automator 库的依赖项引用:
dependencies { ... androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0' }
要优化 UI Automator 测试,您应先检查目标应用的界面组件并确保它们可访问。这些优化提示将在接下来的两部分中进行介绍。
创建 UI Automator 测试类
UI Automator 测试类的编写方式应与 JUnit 4 测试类相同。
在测试类定义的开头添加 @RunWith(AndroidJUnit4.class)
注释。您还需要将 AndroidX Test 中提供的 AndroidJUnitRunner
类指定为默认测试运行程序。
在 UI Automator 测试类中实现以下编程模型:
-
通过调用
getInstance()
方法并将Instrumentation
对象作为参数传递给该方法,获取UiDevice
对象以访问要测试的设备。 -
通过调用
findObject()
方法,获取UiObject
对象以访问设备上显示的界面组件(例如,前台的当前视图)。 -
通过调用
UiObject
方法,模拟需要在该界面组件上执行的特定用户交互;例如,调用performMultiPointerGesture()
以模拟多点触控手势,以及调用setText()
以修改文本字段。您可以根据需要反复调用第 2 步和第 3 步中的 API,以测试涉及多个界面组件或用户操作序列的更复杂的用户交互。 -
执行这些用户交互后,检查界面是否反映了预期的状态或行为。
下面几部分更详细地介绍了这些步骤。
访问界面组件
UiDevice
对象是您访问和操纵设备状态的主要方式。在测试中,您可以调用 UiDevice
方法检查各种属性的状态,如当前屏幕方向或显示屏尺寸。您的测试可以使用 UiDevice
对象执行设备级操作,如强制设备进行特定旋转、按方向键硬件按钮,以及按主屏幕和菜单按钮。
最好从设备的主屏幕开始测试。在主屏幕(或您在设备中选择的其他某个起始位置)上,您可以调用 UI Automator API 提供的方法,以选择特定的界面元素并与之交互。
以下代码段展示了您的测试如何获取 UiDevice
实例并模拟按主屏幕按钮的操作:
import org.junit.Before import androidx.test.runner.AndroidJUnit4 import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.By import androidx.test.uiautomator.Until ... private const val BASIC_SAMPLE_PACKAGE = "com.example.android.testing.uiautomator.BasicSample" private const val LAUNCH_TIMEOUT = 5000L private const val STRING_TO_BE_TYPED = "UiAutomator" @RunWith(AndroidJUnit4::class) @SdkSuppress(minSdkVersion = 18) class ChangeTextBehaviorTest2 { private lateinit var device: UiDevice @Before fun startMainActivityFromHomeScreen() { // Initialize UiDevice instance device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) // Start from the home screen device.pressHome() // Wait for launcher val launcherPackage: String = device.launcherPackageName assertThat(launcherPackage, notNullValue()) device.wait( Until.hasObject(By.pkg(launcherPackage).depth(0)), LAUNCH_TIMEOUT ) // Launch the app val context = ApplicationProvider.getApplicationContext<Context>() val intent = context.packageManager.getLaunchIntentForPackage( BASIC_SAMPLE_PACKAGE).apply { // Clear out any previous instances addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) } context.startActivity(intent) // Wait for the app to appear device.wait( Until.hasObject(By.pkg(BASIC_SAMPLE_PACKAGE).depth(0)), LAUNCH_TIMEOUT ) } }
使用 findObject()
方法检索 UiObject
,它表示符合给定选择器条件的视图。您可以根据需要重复使用已在应用测试的其他部分中创建的 UiObject
实例。请注意,每当您的测试使用 UiObject
实例以点击界面元素或查询属性时,UI Automator 测试框架都会在当前显示内容中搜索匹配项。
以下代码段展示了您的测试如何构建表示应用中的“取消”按钮和“确定”按钮的 UiObject
实例。
val cancelButton: UiObject = device.findObject( UiSelector().text("Cancel").className("android.widget.Button") ) val okButton: UiObject = device.findObject( UiSelector().text("OK").className("android.widget.Button") ) // Simulate a user-click on the OK button, if found. if (okButton.exists() && okButton.isEnabled) { okButton.click() }
指定选择器
如果您需要访问应用中的特定界面组件,请使用 UiSelector
类。此类表示对当前显示的界面中特定元素的查询。
如果找到了多个匹配元素,系统会将布局层次结构中的第一个匹配元素作为目标 UiObject
返回。构建 UiSelector
时,您可以将多个属性链接在一起以优化搜索。如果未找到匹配的界面元素,系统会抛出 UiAutomatorObjectNotFoundException
。
您可以使用 childSelector()
方法来嵌套多个 UiSelector
个实例。例如,以下代码示例展示了您的测试如何指定搜索,以在当前显示的界面中查找第一个 ListView
,然后在该 ListView
中搜索,以查找具有文本属性“Apps”的界面元素。
val appItem: UiObject = device.findObject( UiSelector().className("android.widget.ListView") .instance(0) .childSelector( UiSelector().text("Apps") ) )
最佳做法是,在指定选择器时,应使用资源 ID(如果已将其分配给界面元素),而不是文本元素或内容描述符。并非所有元素都有文本元素(例如,工具栏中的图标)。文本选择器很脆弱,如果界面发生细微更改,可能会导致测试失败。此外,文本选择器也可能无法在不同语言之间扩展,它们可能与翻译的字符串不匹配。
在选择器条件中指定对象状态可能很有用。例如,如果要选择所有已选中元素的列表以便取消选中这些元素,请调用 checked()
方法并将参数设置为 true
。
执行操作
您的测试获取 UiObject
对象后,您可以调用 UiObject
类中的方法,在由该对象表示的界面组件上执行用户交互。您可以指定如下操作:
-
click()
:点击界面元素的可见边界的中心。 -
dragTo()
:将此对象拖动到任意坐标。 -
setText()
:清除可修改字段的内容后,设置该字段中的文本。相反,clearTextField()
方法用于清除可修改字段中的现有文本。 -
swipeUp()
:对UiObject
执行向上滑动操作。同样,swipeDown()
、swipeLeft()
和swipeRight()
方法用于执行相应的操作。
通过 UI Automator 测试框架,您可以发送 Intent
或启动 Activity
,无需使用 shell 命令,只需通过 getContext()
获取 Context
对象即可。
以下代码段展示了您的测试如何使用 Intent
启动被测应用。当您只想测试计算器应用而不关心启动器时,此方法很有用。
fun setUp() { ... // Launch a simple calculator app val context = getInstrumentation().context val intent = context.packageManager.getLaunchIntentForPackage(CALC_PACKAGE).apply { addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) } // Clear out any previous instances context.startActivity(intent) device.wait(Until.hasObject(By.pkg(CALC_PACKAGE).depth(0)), TIMEOUT) }
对集合执行操作
如果需要模拟内容集合(例如,音乐专辑中的歌曲或收件箱中的电子邮件列表)上的用户交互,请使用 UiCollection
类。要创建 UiCollection
对象,请指定 UiSelector
,用于搜索其他子界面元素的界面容器或封装容器,如包含子界面元素的布局视图。
以下代码段展示了您的测试如何构建 UiCollection
以表示 FrameLayout
中显示的视频专辑:
val videos = UiCollection(UiSelector().className("android.widget.FrameLayout")) // Retrieve the number of videos in this collection: val count = videos.getChildCount( UiSelector().className("android.widget.LinearLayout") ) // Find a specific video and simulate a user-click on it val video: UiObject = videos.getChildByText( UiSelector().className("android.widget.LinearLayout"), "Cute Baby Laughing" ) video.click() // Simulate selecting a checkbox that is associated with the video val checkBox: UiObject = video.getChild( UiSelector().className("android.widget.Checkbox") ) if (!checkBox.isSelected) checkBox.click()
对可滚动视图执行操作
使用 UiScrollable
类模拟显示屏上的垂直或水平滚动。当界面元素位于屏幕外而您需要滚动屏幕以使其进入视野时,此方法很有用。
以下代码段展示了如何模拟向下滚动“设置”菜单并点击“关于平板电脑”选项的操作:
val settingsItem = UiScrollable(UiSelector().className("android.widget.ListView")) val about: UiObject = settingsItem.getChildByText( UiSelector().className("android.widget.LinearLayout"), "About tablet" ) about.click()
验证结果
InstrumentationTestCase
扩展了 TestCase
,因此您可以使用标准的 JUnit Assert
方法测试应用中的界面组件是否会返回预期结果。
以下代码段展示了您的测试如何找到计算器应用中的几个按钮,按顺序点击它们,然后验证是否显示了正确的结果。
private const val CALC_PACKAGE = "com.myexample.calc" fun testTwoPlusThreeEqualsFive() { // Enter an equation: 2 + 3 = ? device.findObject(UiSelector().packageName(CALC_PACKAGE).resourceId("two")).click() device.findObject(UiSelector().packageName(CALC_PACKAGE).resourceId("plus")).click() device.findObject(UiSelector().packageName(CALC_PACKAGE).resourceId("three")).click() device.findObject(UiSelector().packageName(CALC_PACKAGE).resourceId("equals")).click() // Verify the result = 5 val result: UiObject2 = device.findObject(By.res(CALC_PACKAGE, "result")) assertEquals("5", result.text) }
四、测试应用组件集成
如果您的应用使用了用户不直接与之互动的组件(如服务或内容提供器),您应验证这些组件在应用中的行为方式是否正确。
在开发此类组件时,您应养成编写集成测试的习惯,以便当您的应用在设备或模拟器上运行时验证组件的行为。
注意:Android 不为 BroadcastReceiver
提供单独的测试用例类。如需验证 BroadcastReceiver
是否正确响应,您可以测试向其发送 Intent
对象的组件。或者,您可以通过调用 ApplicationProvider.getApplicationContext()
来创建 BroadcastReceiver
的实例,然后调用要测试的 BroadcastReceiver
方法(通常,这是 onReceive()
方法)。
测试服务
如果您要实现将本地 Service
作为应用的组件,则应对 Service
进行测试,以确保其不会出现意外行为。您可以创建插桩单元测试以验证 Service
的行为是否正确;例如,服务是否存储和返回有效的数据值并正确执行数据操作。
AndroidX Test 提供了一个 API,用于在隔离的环境中测试 Service
对象。ServiceTestRule 类是一个 JUnit 4 规则,可在单元测试方法运行之前启动服务,并在测试完成后关闭服务。通过使用此测试规则,您可确保始终在测试方法运行之前建立与服务的连接。如需详细了解 JUnit 4 规则,请参阅 JUnit 文档。
注意:ServiceTestRule 类不支持测试 IntentService
对象。如果您需要测试 IntentService
对象,应将逻辑封装在一个单独的类中,并创建相应的单元测试。
创建服务的集成测试
您的集成测试应编写为 JUnit 4 测试类。如需详细了解如何创建 JUnit 4 测试类以及如何使用 JUnit 4 断言方法,请参阅创建插桩单元测试类。
如需创建服务的集成测试,请在测试类定义的开头添加 @RunWith(AndroidJUnit4::class)
注释。您还需要将 AndroidX Test 提供的 AndroidJUnitRunner
类指定为默认测试运行程序。运行插桩单元测试中对此步骤进行了更详细的说明。
接下来,使用 @Rule
注释在测试中创建一个 ServiceTestRule 实例。
@get:Rule val serviceRule = ServiceTestRule()
以下示例展示了如何实现服务的集成测试。测试方法 testWithBoundService
将验证应用是否成功绑定到本地服务,以及服务接口是否正常运行。
@Test @Throws(TimeoutException::class) fun testWithBoundService() { // Create the service Intent. val serviceIntent = Intent( ApplicationProvider.getApplicationContext<Context>(), LocalService::class.java ).apply { // Data can be passed to the service via the Intent. putExtra(SEED_KEY, 42L) } // Bind the service and grab a reference to the binder. val binder: IBinder = serviceRule.bindService(serviceIntent) // Get the reference to the service, or you can call // public methods on the binder directly. val service: LocalService = (binder as LocalService.LocalBinder).getService() // Verify that the service is working correctly. assertThat(service.getRandomInt(), `is`(any(Int::class.java))) }
测试内容提供器
为内容提供程序创建集成测试
在 Android 中,应用将内容提供程序视为提供数据表而其内部构件不可见的 Google Data API。内容提供程序可能具有许多公开常量,但通常公开方法即使有也很少,而且没有公开变量。因此,您应仅根据提供程序的公开成员编写测试。这样设计的内容提供程序在其自身与其用户之间提供了约定。
内容提供程序可让您访问实际用户数据,因此请务必确保在隔离的测试环境中测试内容提供程序。此方法只允许您针对在测试用例中明确设置的数据依赖项运行。这也意味着,您的测试不会修改实际用户数据。例如,您应避免编写由于先前测试遗留了数据而失败的测试。同样,您的测试应避免在提供程序中添加或删除实际联系信息。
如需在隔离环境中测试您的内容提供程序,请使用 ProviderTestCase2
类。此类允许您使用 Android 模拟对象类(如 IsolatedContext
和 MockContentResolver
)访问文件和数据库信息,而不会影响实际用户数据。
您的集成测试应编写为 JUnit 4 测试类。要详细了解如何创建 JUnit 4 测试类以及如何使用 JUnit 4 断言,请参阅创建本地单元测试类。
如需为您的内容提供程序创建集成测试,必须执行以下步骤:
-
将测试类创建为
ProviderTestCase2
的子类。 -
在测试类定义的开头添加
@RunWith(AndroidJUnit4::class)
注释。 -
将 AndroidX Test 提供的
AndroidJUnitRunner
类指定为默认测试运行程序。 -
通过
ApplicationProvider
类设置Context
对象。请参阅以下代码段中的示例。
@Throws(Exception::class) override fun setUp() { super.setUp() context = ApplicationProvider.getApplicationContext<Context>() }
ProviderTestCase2 的工作原理
您可以使用 ProviderTestCase2
的子类测试提供程序。该基类扩展了 AndroidTestCase
,因此它提供了 JUnit 测试框架以及用于测试应用权限的 Android 专用方法。该类最重要的功能是其初始化,在初始化过程中可创建隔离的测试环境。
初始化是在 ProviderTestCase2
的构造函数中完成的,而子类会在其自己的构造函数中调用该过程。ProviderTestCase2
构造函数会创建一个 IsolatedContext
对象,该对象允许执行文件和数据库操作,但会对与 Android 系统的其他交互进行打桩。文件和数据库操作本身发生在设备或模拟器本地的一个带有特殊前缀的目录中。
然后,该构造函数会创建一个 MockContentResolver
用作测试的解析器。
最后,该构造函数会创建一个被测提供程序的实例。这是一个普通的 ContentProvider
对象,但它会从 IsolatedContext
获取所有环境信息,因此只能在隔离的测试环境中工作。在测试用例类中完成的所有测试都针对该隔离对象运行。
为内容提供程序运行集成测试的方式与插桩单元测试相同。
要测试的内容
以下是关于测试内容提供程序的一些具体准则。
-
使用解析器方法进行测试:即使您可以在
ProviderTestCase2
中实例化提供程序对象,也应始终通过适当的 URI 使用解析器对象进行测试。这样做可确保您在测试提供程序时执行的交互与常规应用将会使用的交互相同。 -
以约定的形式测试公开提供程序:如果您打算将您的提供程序公开并使其可供其他应用使用,应以约定的形式对其进行测试。有关如何执行此操作的一些示例如下所示:
-
使用提供程序公开的常量进行测试。例如,查找在提供程序的其中一个数据表中引用列名称的常量。这些常量应始终为提供程序公开定义的常量。
-
测试提供程序提供的所有 URI。提供程序可能会提供多个 URI,每个 URI 引用数据的不同方面。
-
测试无效的 URI:单元测试应故意使用无效的 URI 调用提供程序,并查找错误。良好的提供程序设计是针对无效的 URI 抛出
IllegalArgumentException
。
-
-
测试标准提供程序交互:大多数提供程序都会提供六种访问方法:
query()
、insert()
、delete()
、update()
、getType()
和onCreate()
。您的测试应验证所有这些方法是否有效。内容提供程序主题中对这些方法进行了更详细的说明。文章来源:https://www.toymoban.com/news/detail-617243.html -
测试业务逻辑:如果内容提供程序实现了业务逻辑,应对其进行测试。业务逻辑包括处理无效值、财务或算术计算、消除或合并重复项。文章来源地址https://www.toymoban.com/news/detail-617243.html
到了这里,关于Android Unit Test的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!