Android UI 测试的最佳实践
浓缩咖啡
UI自动化
撰写 UI 测试
WireMock / MockWebServer
OkReplay
咖啡师
卡斯普雷索
结论
在上一篇文章中,我们讨论了最近流行起来或一直是开发标准的 Android 单元测试框架。它们始终位于测试金字塔的底层。在本文中,我们将跳到更高的层次,即 UI 测试框架。
与上一篇文章一样,我不会涉及使用 UI Automator 和 Espresso 编写测试的基础知识,因为这意味着您已经熟悉它们。但我会为您提供一些建议,帮助您更轻松地编写 UI 测试,以及如何解决最常见的问题。使用标准工具并不总是能够解决这些问题,因此各种插件、扩展和框架通常可以提供帮助。我将介绍我自己使用过的,以及如果您还没有使用过的,我可以推荐给您的。
浓缩咖啡
所以,UI 测试。Google 的框架 Espresso 是这方面的黄金标准。Espresso 的文档很多,但简而言之,几乎每个测试都基于以下算法。
- 对于使用ViewMatcher找到的元素
- 做一些ViewAction
- 使用ViewAssertion检查屏幕上显示的结果
@RunWith(AndroidJUnit4::class)
class MainActivityTest {
@Test
fun test_clickRefreshButton_freshItemsAreLoaded() {
onView(withId(R.id.nameEditText)).perform(typeText("Alex"));
onView(withId(R.id.greetButton)).perform(click());
onView(withId(R.id.greetingTextView)).check(matches(withText("Hi, Alex!")));
}
}
通常,点击某个按钮时可能会打开另一个屏幕。为此,我们有另一个工具IntentMatcher,它可以检查某个 Intent 是否已启动。
这四个组件:ViewAction、ViewMatcher、ViewAssertion 和 IntentMatcher,是所有 UI 测试的基础。上面的示例非常简单,但在复杂的屏幕上,如果发生很多事情,测试代码可能会变得非常冗长,阅读起来也会更加困难。为了改进测试的结构和可读性,我们运用了各种设计模式。
测试可读性的设计模式
- 页面对象模式:此模式意味着应用程序的每个屏幕都呈现为一个单独的类,其中包含所有界面元素以及与它们交互的方法。因此,测试场景不依赖于 UI 实现的细节,并且可以轻松适应设计的变化。Kakao和Kaspresso框架都使用了此模式(我将在本文后面讨论)。
- Screenplay 模式:此模式是页面对象模式的改进版本,它添加了另外两个组件:actor 和 ability。actor 是用户在应用中执行操作的角色。ability 决定了 actors 如何与应用交互(例如,通过 Espresso 或 UiAutomator)。此模式允许您编写具有高抽象级别的测试,并更好地展示应用的业务逻辑。
- 机器人模式:此模式与剧本模式类似,但不同于演员和能力,它使用封装了与屏幕交互逻辑的机器人。机器人可以在不同的测试中重复使用,并相互组合。此模式简化了测试结构,避免了代码重复。
使用 Robot 模式编写的 Espresso 代码如下所示:
@RunWith(AndroidJUnit4::class)
class MainActivityTest {
@Test
fun test_clickRefreshButton_freshItemsAreLoaded() {
login {
setEmail("mail@example.com")
setPassword("pass")
clickLogin()
}
home {
checkFirstRow("First item")
clickRefreshItems()
checkFirstRow("First ")
}
}
}
封装了 Espresso 逻辑的机器人将会是这样的:
class HomeRobot {
fun checkFirstRow(text: String) {
onView(withId(R.id.item1)).check(matches(withText(text)))
}
fun clickRefreshItems() {
onView(withId(R.id.button)).perform(click())
}
}
fun home(body: HomeRobot.() -> Unit) {
HomeRobot().apply { this.body() }
}
因此,如果测试失败,我们就会知道哪个步骤出了问题以及该如何处理。
理想情况下,我们的 Espresso 测试必须像单元测试一样简单易读。如果一次测试涵盖由多个屏幕组成的整个流程,则并非总是可行。在这种情况下,不可能高质量地测试某个特定的屏幕。
流动测试与屏幕测试
当您的 Espresso 测试覆盖整个流程时,您的测试将如下所示:
- 打开屏幕A
- 执行操作 1
- 确保操作成功
- 执行操作 2
- 确保屏幕 B已打开
- 执行操作 3
- 确保操作成功
在这里,我们走一条所谓的“快乐路径”,不检查任何极端情况。实际上,这种测试被称为端到端 (e2e) 测试,它必须尽可能地与屏幕实现分离(理想情况下,它不能使用 Espresso 编写,而是使用其他提供更高抽象级别的框架(UI Automator、Appium 或其他框架))。由于其复杂性,此类测试经常会失败,而且很难修复。此外,它们在持续集成 (CI) 上运行成本相当高,可能持续几分钟甚至几小时,因此您不会希望在每个拉取请求 (PR) 上都运行它们。这就是为什么一个项目中不能有很多这样的测试。相反,
我们可以有更多原子 UI 测试,它们只测试特定的屏幕。这样的测试将包含一组简单的操作:
- 在onBefore中打开一个屏幕
- 执行操作 1
- 确保操作成功
这类测试有很多,它们既能覆盖理想路径,也能涵盖各种极端情况。而且,这类测试通常更稳定。由于其简单性,出错导致测试失败的可能性要小得多。不过,有时你可能会遇到这样的情况:你的测试正确,它所涵盖的业务逻辑也正确。你期望测试在 100% 的情况下都能通过,但实际上只有 1% 的情况下是失败的。这样的测试被称为“不稳定的”。
片状
不稳定测试的主要问题可能在于网络和其他后台操作。问题是,当我们在测试中执行某个操作(例如,点击按钮)并期望得到某个结果时,结果可能会延迟。默认情况下,Espresso 框架可以等待各种操作的完成,但它只处理与 UI 交互的操作(例如,另一个 Activity 以动画方式打开)。Espresso 对与我们的业务逻辑相关的后台操作一无所知。这可能导致测试失败,onView(withId(R.id.item1)).check(matches(withText(text)))
因为预期的文本尚未加载或显示。然而,测试并非总是会失败,只有当运行测试的模拟器上的网络连接速度较慢时才会失败。这可能是不稳定测试最常见的问题之一。有多种方法可以解决这个问题:
- 在我们的测试中添加 Thread.sleep(…)。这是一种蛮力方法,在大多数情况下都会有所帮助,但是,首先,我们无法避免延迟时间比 sleep 更长的情况,这样测试仍然会失败。此外, sleep 会在每次测试运行中添加不必要的延迟,即使服务器运行速度足够快,我们的测试仍然会运行得比我们需要的更长。
-
为 ViewMatcher 添加超时和重试功能。如下所示:
fun onViewWithTimeout( retries: Int = 10, retryDelayMs: Long = 500, retryAssertion: ViewAssertion = matches(withEffectiveVisibility(Visibility.VISIBLE)), matcher: Matcher<View>, ): ViewInteraction { repeat(retries) { i -> try { val viewInteraction = onView(matcher) viewInteraction.check(retryAssertion) return viewInteraction } catch (e: NoMatchingViewException) { if (i >= retries) { throw e } else { Thread.sleep(retryDelayMs) } } } throw AssertionError("View matcher is broken for $matcher") }
Kaspresso 框架就采用了这种方法,我将在下文中讨论。这比添加 好得多Thread.sleep()
,但它仍然不能保证你设置的超时时间会比服务器延迟更长。此外,这样的代码会隐藏一些运行缓慢的代码片段,因此,与其引入超时和重试,不如先研究一下服务器响应时间过长的原因,以及是否应该从另一个角度来解决这个问题。
空闲资源
如上所述,Espresso 在 UI 层面了解空闲状态,也就是说,只有当前一个 ViewAction 完成并且系统进入空闲状态后,测试中才会启动下一个 ViewAction。但是,如果您有一些协程或 Rx Observable 在后台运行并异步返回结果,我们需要以某种方式通知 Espresso,我们希望等待操作完成,并且在此之后才执行下一个ViewAction/ViewAssertion 。您可以在官方文档中详细了解这一点。这里我仅提供一些在实践中有用的提示。
-
你的生产代码不应该知道任何关于 IdlingResource 的信息。你的应用中可能有一些接口
interface OperationStatus { fun finished() fun reset() }
并在app中转到这个界面,告知测试操作完成:
coroutineScope.launch(coroutineDispatcher) { viewModel.usersFlow.collect { // show UI operationStatus.finished() } }
在androidTest中,你将拥有此接口的实现,该实现将了解 IdlingResource。相应地,你将能够在 IdlingRegistry 中注册它。
class OperationStatusIdlingResource : OperationStatus { val idlingResource = CountingIdlingResource("op-status") override fun finished() { idlingResource.decrement() } override fun reset() { idlingResource.increment() } } @Test fun test_clickRefreshButton_freshItemsAreLoaded() { val idlingResourceImpl = OperationStatusIdlingResource() IdlingRegistry.getInstance().register(idlingResourceImpl.idlingResource) // Test }
假设它只存在于测试中,那么该如何将你的代码传递
OperationStatusIdlingResource
给应用程序呢?这里,第二个原则将帮助我们。 -
使用依赖注入 (DI)。无论您使用 Hilt、Dagger 还是 Koin,您始终都会拥有一个依赖关系树和一个模块列表,用于声明这些依赖关系(在我们的例子中是
OperationStatus
)。对于生产代码,您需要创建一个默认的虚拟实现,它不会执行任何操作;对于测试,您需要覆盖源依赖项所在的模块,以便 DI 树能够与其协同工作。我将在下文解释如何覆盖 DI 依赖关系。 -
不要在特殊情况下使用 IdlingResource。在上面的例子中,我们用它来表示数据在屏幕打开时已加载。这是异步数据上传的一个特例。即使在一个屏幕内,也可能有多个异步操作,为每个操作单独创建 IdlingResource 是多余的。最好能识别出引入并发的地方。例如,如果你的应用基于协程,那么在使用
Dispatchers.Default
和时就会引入异步性Dispatchers.IO
。这意味着,在测试中,你需要用某个测试版本替换这些调度器,并在其中添加 IdlingResource:class SimpleViewModel( private val usersRepository: UsersRepository, private val coroutineScope: LifecycleCoroutineScope, private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.Default, ) : ViewModel() { fun loadUsers(filter: FilterType) { coroutineScope.launch(coroutineDispatcher) { val allUsers = usersRepository.getUsers() // ... } }
我们可以通过 DI 在测试中传递以下 Dispatcher:
class IdlingDispatcher( private val wrappedCoroutineDispatcher: CoroutineDispatcher, ) : CoroutineDispatcher() { val counter: CountingIdlingResource = CountingIdlingResource( "IdlingDispatcher for $wrappedCoroutineDispatcher" ) override fun dispatch(context: CoroutineContext, block: Runnable) { counter.increment() val blockWithDecrement = Runnable { try { block.run() } finally { counter.decrement() } } wrappedCoroutineDispatcher.dispatch(context, blockWithDecrement) } }
在 DI 中使用 Fake 对象
这是另一个应该在测试中使用的实用做法。顺便说一句,如果你的项目中还没有使用 DI,那么你应该开始使用它了。
在上面的示例中,我描述了如何在生产代码中使用 IdlingResource 的伪实现,但尚未讨论如何在测试中引入它们。让我们以 Dagger 为例更详细地介绍一下。
如果您不使用 dagger-android 并且更喜欢手动创建组件,那么您的应用程序将或多或少看起来像这样:
open class MyApplication : Application() {
private lateinit var appComponent: ApplicationComponent
override fun onCreate() {
super.onCreate()
appComponent = DaggerApplicationComponent
.builder()
.usersModule(UsersModule())
.dataModule(DataModule())
.build()
}
}
在中DataModule
,我们声明了我们的调度程序,在中UsersModule
,我们定义了与相关的逻辑UsersRepository
。
@Module
open class DataModule {
@Provides
@MyIODisptcher
open fun provideIODispatcher(): CoroutineDispatcher = Dispatchers.IO
请注意MyApplication
,DataModule
和provideIODispatcher
被声明open
,以便可以在测试中从它们继承。
现在,将模块的创建DataModule
转移到单独的方法:
open class MyApplication : Application() {
private lateinit var appComponent: ApplicationComponent
override fun onCreate() {
super.onCreate()
appComponent = DaggerApplicationComponent
.builder()
.usersModule(UsersModule())
.dataModule(createDataModule())
.build()
}
open fun createDataModule() = DataModule()
然后在androidTest文件夹中,创建一个测试类Application
,并DataModule
在其中重新定义。
class MyTestApplication: MyApplication() {
override fun createDataModule() = TestDataModule()
}
class TestDataModule {
override fun provideIODispatcher(): CoroutineDispatcher = IdlingDispatcher()
}
在中provideIODispatcher
,我们创建了上面讨论过的我们的一个实例IdlingDispatcher
,现在,它将在所有 UI 测试中默认使用。
但这还不够。我们需要注册测试应用,以便它能与测试一起运行。为此,我们需要创建一个自定义的 TestRunner,并在其中传递测试应用的名称。
class MyApplicationTestRunner: AndroidJUnitRunner() {
override fun newApplication(cl: ClassLoader?, className: String?, context: Context?): Application {
return super.newApplication(cl, MyTestApplication::class.java.name, context)
}
}
现在,我们在以下位置注册此 TestRunner build.gradle
:
android {
namespace 'com.rchugunov.tests'
compileSdk 33
defaultConfig {
applicationId "com.rchugunov.tests"
minSdk 26
targetSdk 33
versionCode 1
versionName "1.0"
testInstrumentationRunner "com.rchugunov.tests.MyApplicationTestRunner"
}
这就是我们所需要的。类似于IdlingDispatcher
,我们也可以覆盖其他依赖项,用伪副本替换它们。例如,对于 UserRepository,这样的实现可能如下所示:
class FakeUserRepository: UsersRepository {
var usersToOverride = listOf(
User(id = "1", userName = "jamie123", age = 10),
User(id = "2", userName = "christy_a1", age = 34)
)
override suspend fun getUsers(): List<User> {
return usersToOverride
}
}
现在,当您需要放置自定义用户列表时,您可以FakeUserRepository
直接将其注入测试,并设置必须直接返回 ViewModel 的列表usersToOverride
。如果您只想测试表示层而不测试数据层,这将非常有用。另一个好处是,由于不会出现服务器请求延迟,测试运行速度会更快。下面,我将介绍如何使用Wiremock和OkReplay模拟客户端-服务器逻辑。
与 Dagger 类似,您还可以在Hilt和Koin中提供测试实现。
在编写和使用 UI 测试时,还有什么方法能让你的工作更轻松呢?那就从Robolectric开始吧。
机器人电工
Robolectric是一个相当古老的框架,可以追溯到 arm-v7 模拟器在 x86 机器上运行的时代。由于它运行速度非常慢,开发者们想出了提取 Android AOSP 并将其编译成 jar 文件的想法,然后像在真机上一样对其进行 Espresso 测试。由于这些测试实际上是在本地计算机上运行的(与 JUnit 测试相同),因此它比在模拟器或设备上进行相同的测试要快得多。
Robolectric 使用起来非常简单;你只需要在现有的 Espresso 测试中添加几行代码即可。以下是官方页面上的一个测试示例:
@RunWith(RobolectricTestRunner::class)
class MyActivityTest {
@Test
fun clickingButton_shouldChangeMessage() {
Robolectric.buildActivity(MyActvitiy::class.java).use { controller ->
controller.setup() // Moves Activity to RESUMED state
val activity: MyActvitiy = controller.get()
activity.findViewById(R.id.button).performClick()
assertEquals((activity.findViewById(R.id.text) as TextView).text, "Robolectric Rocks!")
}
}
}
使用 Robolectric 有很多优点,但最主要的是测试速度。然而,也存在一些限制:例如,它不能与设备的传感器、系统按钮和位置服务一起使用。另外,不要忘记您只是在处理一个模拟的 Android 实现。实际上,您的代码可能在 Robolectric 环境中运行,但由于某种原因会在模拟器上失败。根据 Jake Wharton 的说法,只有当您确信知道测试代码在后台如何运行时,您最好才使用 Robolectric。我不建议使用 Robolectric 运行涵盖整个流程或用户与 UI 交互的测试。以下是一些如何使用 Robolectric 的示例:
-
您可以测试应用中使用数据层的各个组件。例如,您可以使用 Room DAO 进行测试。
- 将对象插入数据库
- 从数据库中获取具有相同 ID 的对象
- 检查是否返回了相同的对象。
使用 Robolectric 编写的测试对于此来说是理想的。
-
打开深层链接。在这里,您可以启动广播事件并检查具有特定参数集的某个意图是否已打开。
-
使用文件系统。这是应用的数据层,因此您可以将其与流程的其余部分隔离开来测试。在这种情况下,您可能需要 Context,而 Robolectric 正是提供 Context 的工具。
因此,Robolectric 和 Espresso 可以协同工作,帮助您测试应用的各个组件以及整个屏幕和流程。然而,有些场景它们无法覆盖。例如,当我们需要最小化应用、进入系统设置或授予应用某些运行时权限时。在这种情况下,UI Automator就是您的救星。
UI自动化
Espresso 测试有一个重要特性——它们应该了解正在测试的生产代码。您可以接收某个类对象的引用,或在测试中注入应用程序的虚拟组件。您可以访问应用程序的资源(R.id、R.string 等等)。因此,您可以编写非常灵活的测试,以适应应用程序的逻辑。此外,您还可以更改应用程序的逻辑,使其在测试运行时的工作方式略有不同。
相反,UI Automator 测试会像用户一样看待您的应用。它们可以看到文本字段、按钮,并且可以与 UI 元素交互,但它们不知道应用的内部逻辑和状态。您无法更改应用的逻辑或访问某些资源。然而,使用 UI Automator,您可以执行以下操作:
-
与系统应用和设置(例如主屏幕、通知和设备设置)交互。例如,您可以这样访问系统通知列表:
@Test @Throws(UiObjectNotFoundException::class) fun testNotifications() { device.openNotification() device.wait(Until.hasObject(By.pkg("com.android.systemui")), 10000) val notificationStackScroller: UiSelector = UiSelector() .packageName("com.android.systemui") .resourceId("com.android.systemui:id/notification_stack_scroller") val notificationStackScrollerUiObject: UiObject = device.findObject(notificationStackScroller) assertTrue(notificationStackScrollerUiObject.exists()) val notiSelectorUiObject: UiObject = notificationStackScrollerUiObject.getChild(UiSelector().index(0)) assertTrue(notiSelectorUiObject.exists()) notiSelectorUiObject.click() }
-
Android UI Automator 可以测试复杂的场景,包括应用间的切换,例如内容交换或使用 Intent。Espresso 只能在一个应用内测试场景,无法处理应用间或 Intent 间的切换。
-
您可以在运行测试时直接检查或更改系统设置。本文介绍如何在测试中连接 Wi-Fi。
// BySelector matching the just added Wi-Fi val ssidSelector = By.text(ssid).res("android:id/title") // BySelector matching the connected status val status = By.text("Connected").res("android:id/summary") // BySelector matching on entry of Wi-Fi list with the desired SSID and status val networkEntrySelector = By.clazz(RelativeLayout::class.qualifiedName) .hasChild(ssidSelector) .hasChild(status) // Perform the validation using hasObject // Wait up to 5 seconds to find the element we're looking for val isConnected = device.wait(Until.hasObject(networkEntrySelector), 5000) Assert.assertTrue("Verify if device is connected to added Wi-Fi", isConnected)
如您所见,UI Automator、Espresso 和 Robolectric 提供了以独立方式测试应用组件的机会,并检查非常复杂的流程,包括与其他应用和 Android 组件的交互。顺便说一句,您还可以组合测试,将 Espresso 测试与 UI Automator 测试结合起来。
撰写 UI 测试
那么 Compose 呢?为了测试 Compose,有一组特殊的 API可以将可组合项视为单独的节点。它还包含选择器和操作,您可以使用它们查找界面元素并对其执行某些操作。
composeTestRule.onNode(hasTestTag("Players"))
.onChildren()
.filter(hasClickAction())
.assertCountEquals(4)
.onFirst()
.assert(hasText("John"))
好消息是,所有这些 API 都与 UI 相关,类似于 Espresso 的 ViewMatchers/ViewActions/ViewAssertions。这意味着您的测试在语法上只会略有不同,但您仍然可以使用 Robot 或 Page Object 模式来解决代码整洁问题。为了同步后台任务和测试,您仍然可以使用 IdlingResource。此外,您可以像我们在 Espresso 示例中所做的那样,替换 DI 树中的各种对象。
此外,您仍然可以使用 Espresso API 来测试您的应用与 Android 框架的集成,例如导航、动画和对话框窗口。
@Test
fun androidViewInteropTest() {
// Check the initial state of a TextView that depends on a Compose state:
Espresso.onView(withText("Hello Views")).check(matches(isDisplayed()))
// Click on the Compose button that changes the state
composeTestRule.onNodeWithText("Click here").performClick()
// Check the new value
Espresso.onView(withText("Hello Compose")).check(matches(isDisplayed()))
}
另外,您还可以找到一些介绍如何在 Robolectric 上运行ComposeUI 测试的文章。我个人没有这样做过,因为我不喜欢在模拟器之外测试 UI 逻辑。
WireMock / MockWebServer
还有什么可以帮助我们编写测试?可以模拟网络请求的框架。我们已经讨论过,通过创建虚假对象并将其传递给 DI 树,我们可以模拟部分业务逻辑,并仅测试高级逻辑(表示层)。然而,在某些情况下,一次性覆盖应用程序所有层的测试仍然有用。这样一来,你可能会遇到一些问题,例如服务器不稳定或所需条件的复杂复制。所有这些都会使你的测试变得不稳定——我们上面已经讨论过这个问题。幸运的是,有些框架允许你模拟客户端-服务器部分。
Wiremock和MockWebServer提供了类似的功能来替代客户端/服务器交互。我们以 MockWebServer 为例。
在运行每个测试之前,我们必须启动服务器,并在测试完成后停止它。创建自定义测试规则 (TestRule) 可以方便地做到这一点。
class MockWebServerRule : TestRule {
lateinit val server: MockWebServer
override fun apply(base: Statement, description: Description): Statement {
return object : Statement() {
@Throws(Throwable::class)
override fun evaluate() {
val server = MockWebServer()
server.start(8080)
try {
base.evaluate()
} finally {
server.shutdown()
}
}
}
}
}
如果您希望请求按照一定的顺序执行,那么您可以在测试中对它们进行排队。
@RunWith(AndroidJUnit4::class)
class MyEspressoTest {
@get:Rule
val mockWebServerRule = MockWebServerRule()
@Test
fun test_some_action() {
mockWebServerRule.apply {
server.enqueue(MockResponse().setBody("..."))
server.enqueue(MockResponse().setBody("Hello world!"))
server.enqueue(MockResponse().setResponseCode(401))
}
// your test case
}
}
记得为 Retrofit 提供测试 BaseUrl 127.0.0.1
。你可以按照我们上面保存伪造文件时的方法进行UserRepository
操作TestApplicationComponent
。
之后,您可以运行测试,响应您应用的所有请求,您添加到队列中的响应将会返回。请注意,请求的数量和顺序必须与测试中设置的响应数量严格匹配。否则,测试肯定会失败。您还可以使用请求调度器编写更精细的逻辑来处理应用的请求Dispatcher
:
@RunWith(AndroidJUnit4::class)
class MyEspressoTest {
@get:Rule
val mockWebServerRule = MockWebServerRule()
@Test
fun test_mock_with_dispatcher() {
val requests = listOf(MockRule.USERS_REQUEST_FAILED_RESPONSE)
mockWebServerRule.server.dispatcher = object : Dispatcher() {
override fun dispatch(request: RecordedRequest): MockResponse {
return requests.first { it.content.url == request.requestUrl }.content.response
}
}
// YOUR TEST CODE
}
data class MockRuleContent(
val url: String,
val response: MockResponse,
)
enum class MockRule(val content: MockRuleContent) {
USERS_REQUEST_POSITIVE_RESPONSE(MockRuleContent("/users/", MockResponse().setBody("[{\"name\": \"John\"}]"))),
USERS_REQUEST_FAILED_RESPONSE(MockRuleContent("/users/", MockResponse().setResponseCode(404)))
}
WireMock具有类似的功能,但它比 MockWebServer 更难使用,而且 Android 社区的支持也不那么强大。此外,WireMock 还有一个重要特性:您可以以记录模式运行服务器,这样您就可以在在线客户端/服务器交互模式下运行测试。WireMock 可以写入来自服务器的所有响应并将其保存到一个文件中。之后,您将能够使用已经记录的模拟运行相同的测试。MockWebServer 无法做到这一点,但OkReplay非常适合这项任务。
OkReplay
使用 OkReplay,您可以根据真实的服务器请求准备测试存根(类似于 WireMock)。要使用它,您需要OkReplayInterceptor
在 Retrofit/OkHttp 测试配置中添加拦截器。然后,使用 Gradle 插件,您可以以将服务的请求和响应记录到 .yaml 文件(称为 Tape)的模式运行测试。此外,OkReplay 还提供了一个 Gradle 插件,其中包含从设备或模拟器中提取已录制磁带以及清理磁带的任务。
./gradlew clearDebugOkReplayTapes - Cleaning tapes
./gradlew pullDebugOkReplayTapes - Pulling tapes from the device or emulator
为了在磁带录制或重放模式下运行测试,您需要将相应的参数TapeMode传递到OkReplay配置中:
private val activityTestRule = ActivityTestRule(MainActivity::class.java)
private val configuration = OkReplayConfig.Builder()
.tapeRoot(AndroidTapeRoot(InstrumentationRegistry.getInstrumentation().targetContext, javaClass))
.defaultMode(TapeMode.READ_WRITE) // или TapeMode.READ_ONLY
.sslEnabled(true)
.interceptor(okReplayInterceptor)
.build()
@JvmField
@Rule
val testRule = OkReplayRuleChain(configuration, activityTestRule).get()
@Test
@OkReplay
fun myTest() {
...
}
OkReplay 框架简化了 Android 应用的网络请求测试流程,确保测试结果更安全、更可预测。然而,还有一个重要因素:你需要测试能够真实重现应用行为的场景(例如,服务器端的特定错误)。重现这些情况通常非常困难,因此录制磁带存在问题。
开发人员一直在努力解决上述所有问题。你可以在 Github 上找到许多开源库,它们不仅封装了 Espresso API,还有助于解决其中的一些问题,并添加了各种令人愉悦的功能。我将向你介绍其中的两个:Barista和Kaspresso。
咖啡师
Barista 是 Espresso 的额外抽象层,因此与 Espresso 相比,它具有一些额外的功能。首先,它添加了许多方法,以便更轻松地处理 UI 元素。
例如,原始 Espresso 代码如下:
@Test
fun myTest() {
onView(withId(R.id.first_name))
.perform(typeText(FIRST_NAME), ViewActions.closeSoftKeyboard())
onView(withId(R.id.second_name))
.perform(typeText(SECOND_NAME), ViewActions.closeSoftKeyboard())
onView(withId(R.id.save)).check(matches(isEnabled()))
onView(withId(R.id.save)).perform(click())
// write your test as usual...
}
我们可以这样写:
@Test
fun myTest() {
writeTo(R.id.first_name, FIRST_NAME)
closeKeyboard()
writeTo(R.id.second_name, SECOND_NAME)
closeKeyboard()
assertEnabled(R.id.save);
clickOn(R.id.save)
assertDisplayed(FIRST_NAME)
}
不可否认的是,测试的可读性有所提升。缺点是,与常规 Espresso 相比,你需要记住更多不同的 ViewMatcher/ViewAction 和其他元素。不过,你仍然可以使用 Robot 模式来提升测试的表达能力。你可以在这里了解更多可用方法。Barista 还提供了许多便捷的测试规则,例如针对数据库和SharedPreferences
清理的规则:
// Clear all app's SharedPreferences
@Rule public ClearPreferencesRule clearPreferencesRule = new ClearPreferencesRule();
// Delete all tables from all the app's SQLite Databases
@Rule public ClearDatabaseRule clearDatabaseRule = new ClearDatabaseRule();
// Delete all files in getFilesDir() and getCacheDir()
@Rule public ClearFilesRule clearFilesRule = new ClearFilesRule();
但是,究竟应该使用它们还是自己编写所有内容,这是一个好问题。我们的应用程序通常非常复杂,无法使用标准工具,因此每个开发人员都倾向于编写自己的自定义逻辑。
卡斯普雷索
另一个由卡巴斯基反病毒软件开发人员创建的 Espresso 包装器。但这个框架提供的功能远多于 Barista。首先,它默认允许你使用页面对象模式编写测试。这是一个不可否认的优势,因为测试看起来更简洁,并且更抽象地脱离了所使用的视图及其 ID。
object SimpleScreen : KScreen<SimpleScreen>() {
override val layoutId: Int? = R.layout.activity_simple
override val viewClass: Class<*>? = SimpleActivity::class.java
val button1 = KButton { withId(R.id.button_1) }
val button2 = KButton { withId(R.id.button_2) }
val edit = KEditText { withId(R.id.edit) }
}
Kaspresso 的另一个重要功能是所有 ViewAction 都包含一些超时设置,以应对不稳定的测试,这在我们等待后端响应的情况下非常有用。这可能很方便,但不太可靠,因为设置超时有时并不够。我建议更多地依赖 IdlingResource 和使用 OkReplay 或服务器响应模拟的预定义服务器响应。
此外,Kaspresso 还提供许多其他实用功能,例如在测试中直接运行 adb 提示以及与 Android 系统交互。考虑到所有这些功能都包含在现成的解决方案中,Kaspresso 是传统 Espresso 的绝佳替代品。
结论
和许多其他开发者一样,我在编写 Espresso 测试时遇到了很多困难。这些测试通常很复杂、缓慢且不稳定。然而,现在有很多库、框架和方法可以显著简化和加快 UI 测试的编写过程。如果我现在开始开发一个新应用,我会立即使用 Kaspresso 编写 UI 测试。IdlingResource 对于同步后台任务和测试本身至关重要。如果可能,请使用存储库的模拟实现或使用 OkReplay 记录请求和响应。使用页面对象和 Robot 模式来保持测试的整洁。如果您遵循这些建议,您将能够显著提高测试质量并减少 Android 应用代码中的错误数量。
鏂囩珷鏉ユ簮锛�https://dev.to/rchugunov/best-practices-of-ui-testing-for-android-5756