在previous article中,我们讨论了最近变得流行或始终是开发标准的Android的单元测试框架。它们总是置于测试金字塔的基础上。在本文中,我们将跳到一个更高的级别,即UI测试框架。
与上一篇文章一样,我将使用UI Automator和Espresso撰写测试的基础知识,因为这意味着您已经熟悉了它们。但是,我将为您提供一些建议,以使您在编写UI测试时如何更轻松,以及如何解决最常见的问题。并非总是可以用标准工具来解决它们,因此各种插件,扩展程序和框架通常会进行营救。我将介绍我与自己一起工作的那些,如果您还没有与他们一起工作,我可以向您推荐。
浓咖啡
So,UI测试。 Google框架浓缩咖啡是这里的gold standard。浓缩咖啡有很多文档,但简而言之,几乎每个测试都基于以下算法。
- 对于使用 ViewMatcher 找到的元素
- 做一些视图
- 使用 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 ,可以检查是否启动了某些意图。
这四个组件, ViewAction,ViewMatcher,ViewAssertion,IntentMatcher,是所有UI测试的基础。上面的示例非常简单,但是在复杂的屏幕上发生了很多事情,我们的测试的身体可以大大增长,并且很难阅读它。为了提高测试的结构和可读性,应用了各种设计模式。
测试可读性的设计模式
- Page Object Pattern:此模式意味着应用程序的每个屏幕都作为一个单独的类显示,其中包含所有接口元素和与它们交互的方法。因此,测试方案不取决于UI实现的详细信息,并且可以轻松适应设计的更改。此模式用于框架Kakao和Kaspresso(我将在本文稍后讨论)。
- 剧本模式:此模式是页面对象模式的改进版本,添加了另外两个组成部分:参与者和能力。演员是在应用程序中执行操作的用户角色。能力决定了参与者如何与应用程序交互的能力(例如,通过浓缩咖啡或UIAUTOMator)。这种模式使您可以用高水平的抽象来编写测试,并更好地显示应用程序的业务逻辑。
- Robot Pattern:此模式类似于剧本模式,但是,使用了演员和能力的整体,使用了封装与屏幕相互作用的逻辑的机器人。机器人可以在不同的测试中重复使用并相互结合。这种模式简化了测试的结构,并使您免于代码重复。
用机器人模式编写的浓缩咖啡代码如下:
@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 ")
}
}
}
和封装浓缩咖啡逻辑的机器人看起来像这样:
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() }
}
因此,如果测试失败,我们会知道某事发生了什么步骤,该怎么办。
理想情况下,我们的浓缩咖啡测试必须像单位测试一样简单且可读。如果您一次仔细检查一个由多个屏幕组成的整个流程,这并不总是可能。在这种情况下,无法测试一个具有高质量的特定屏幕。
流程测试与屏幕测试
当您的浓缩咖啡测试涵盖整个流程时,您的测试将如下所示:
- 打开屏幕
- 做行动1
- 确保动作成功
- 做行动2
- 确保屏幕b 打开
- 做行动3
- 确保动作成功
在这里,我们经过所谓的快乐路径,不要检查任何角落案例。实际上,该测试称为端到端(E2E)测试,必须尽可能与屏幕实现分开(理想情况下,它必须不使用浓缩咖啡来编写,而是在其他框架中提供更高级别的抽象(UI Automator,Appium或其他类似))。由于它们的复杂性,这种测试通常会失败,而且很难修复它们。另外,它们在CI上运行非常昂贵,可以运行几分钟甚至数小时,因此,这不是您想要在每个拉的请求下运行的东西。这就是为什么项目中可能会有很多这样的测试。
相反,我们可以进行更多的原子UI测试,该测试仅测试特定的屏幕。这样的测试将包含一组简单的操作:
- 在之前打开屏幕
- 做行动1
- 确保动作成功
可能会有很多这样的测试,它们可以涵盖快乐的道路和各种角落案例。同样,此类测试通常更稳定。由于它们的简单性,出现问题和测试失败的机会远不超过。尽管有时您可能会有正确的测试情况,并且其涵盖的业务逻辑也是正确的。您预计该测试在100%的情况下将是绿色的,但事实证明在100例中有红色。此类测试称为 flaky。
片状
可能,片状测试问题的主要来源是网络和其他背景操作。问题是,当我们在测试中执行某个操作(例如,单击一个按钮)并期望某个结果时,可能会延迟此结果。默认情况下,意式浓缩咖啡框架可以等待各种操作的完成,但这是关于仅使用UI交互的操作(例如,当另一个活动与动画打开时)。浓缩咖啡对与我们的业务逻辑相关的背景操作一无所知。这会导致test onView(withId(R.id.item1)).check(matches(withText(text)))
的可能故障,因为尚未加载或显示预期的文本。但是,测试并不总是失败,而只有在测试正在运行的模拟器上的Internet连接缓慢时,才会失败。这也许是片状测试中最常见的问题之一。有各种方法可以解决它:
- 将螺纹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()
要好得多,但是仍然可以保证您设置的超时时间将比服务器延迟更长。此外,此类代码隐藏了您的代码的缓慢部分,这就是为什么,而不是引入超时和重试的原因,而是研究服务器在这个地方响应这么长时间的原因,以及您是否应该从其他方面处理问题的原因。<<<<<<<<<< /p>
iDlingresource
如上所述,意式浓缩咖啡知道在UI级别的空闲状况,这是您测试中的下一个视图只有在上一台完成并且系统到达闲置状态时才会启动。但是,如果您有一些可观察到的Coroutine或RX可观察到的,并且在后台运行并以异步返回结果,我们需要以某种方式告知浓缩咖啡,我们想等待完成操作完成并执行下一个 ViewsAction/ViewAssertion 仅在那之后。您可以在official documentation中详细了解此信息。在这里,我会给您一些提示,可以在实践中有所帮助。
-
您的生产代码不应该对iDlingresource了解一无所知。您的应用程序可能有一些接口
interface OperationStatus { fun finished() fun reset() }
并转到应用程序中的此接口,以告知测试操作已完成:
coroutineScope.launch(coroutineDispatcher) { viewModel.usersFlow.collect { // show UI operationStatus.finished() } }
和 androidTest 中,您将实现此接口,以了解IDLINGRESOURCE。相应地,您将能够在indlingregistry中注册。
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,都过多。如果您确定引入并发的地方,那就更好了。例如,如果您的应用基于Coroutines,则使用
Dispatchers.Default
和Dispatchers.IO
时引入异步性。这意味着,在测试中,您需要用一些测试版本替换这些调度程序,并将其添加到它:
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通过测试中的以下调度程序:
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中使用假物体
应在测试中使用的另一种有用的实践。顺便说一句,如果您在项目中不使用DI,则应开始这样做。
在上面的示例中,我描述了如何在我们的生产代码中使用iDlingResource的虚假实现,但是没有讨论如何将它们引入测试中。让我们以匕首为例更详细地介绍它。
如果您不使用匕首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测试中使用。
但这还不够。我们需要注册我们的测试应用程序,以便它与测试一起运行。为此,我们需要创建一个自定义测试程序,我们将通过测试应用程序的名称。
class MyApplicationTestRunner: AndroidJUnitRunner() {
override fun newApplication(cl: ClassLoader?, className: String?, context: Context?): Application {
return super.newApplication(cl, MyTestApplication::class.java.name, context)
}
}
现在,我们在build.gradle
中注册了此testrunner:
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的kude27列表。如果您只想在没有数据层的情况下测试演示层,这将很有用。另一个优势是,由于服务器请求不会延迟测试,测试将更快。下面,我将告诉您如何使用 wiremock 和 okreplay 。
在写作和使用UI测试时,您还能如何使自己的生活更轻松?开始使用 robolectric 。
robolectric
Robolectric是一个相当古老的框架,它可以追溯到x86机器上ARM-V7模拟器运行的时代。它非常慢,开发人员提出了提取Android AOSP并将其编译到JAR文件中的想法,然后像在真实机器上一样对其进行浓缩咖啡测试。由于测试实际上是在本地计算机上运行的(与JUNIT测试相同),因此其工作速度远比模拟器或设备上的同一测试要快得多。
robolectric非常易于使用;您只需要在现有的浓缩咖啡测试中添加几行。这是官方页面测试的示例:
@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环境中起作用,但由于某种原因将在模拟器上失败。 According to Jake Wharton,只有当您确定您的测试代码如何在引擎盖下运行时,才最好使用Robolectric。我不建议运行测试,以涵盖与UI的整个流量或用户交互,并与Robolectric进行。这是您应该如何使用robolectric的几个示例:
-
您可以测试使用数据层的应用程序的各个组件。例如,您可以使用dao Room。
测试工作。- 将对象插入数据库
- 从数据库中获取具有相同ID的对象
- 检查同一对象是否返回。
用robolectric编写的测试将是理想的选择。
-
打开深链接。在这里,您可以启动广播事件,并检查是否打开了一组参数的某些意图。
-
使用文件系统。这是应用程序的数据层,因此您可以与其他流的其余部分隔离测试。在这种情况下,您可能需要上下文,而robolectric是可以提供它的工具。
因此,robolectric和浓缩咖啡共同帮助您测试应用程序的两个组件以及整个屏幕和流程。但是,有些场景无法涵盖。例如,当我们需要最大程度地减少应用程序时,转到系统设置或授予应用程序的运行时许可。在这种情况下, UI Automator 是您的救星。
Wiques Automator
浓缩咖啡测试具有一个重要的功能 - 他们应该了解他们正在测试的生产代码。您可以在测试中收到对某些类对象的引用或将应用程序的伪造组件注入。您可以访问该应用程序的资源(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可以测试包括在应用程序之间切换的复杂场景,例如,内容交换或使用意图。浓缩咖啡只能在一个应用程序中测试方案,并且无法处理应用程序或意图之间的切换。
-
运行测试时,您可以直接检查或更改系统设置。 This article涵盖了如何在测试中连接到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组件进行交互。顺便说一句,您还可以将测试结合在一起,并与UI Automator测试一起进行浓缩咖啡测试。
组成UI测试
撰写呢?为了进行测试,有一个特殊的set of APIs将组合视为单个节点。它还包括选择器和操作,您可以找到UI元素并与它们一起执行某些操作。
composeTestRule.onNode(hasTestTag("Players"))
.onChildren()
.filter(hasClickAction())
.assertCountEquals(4)
.onFirst()
.assert(hasText("John"))
所有这些API都与UI有关,类似于Espresso的ViewMatchers/ViewActions/ViewAssertions。这意味着您的测试仅在语法中略有不同,但是您仍将使用模式机器人或页面对象解决代码清洁度问题。为了同步您的背景任务和测试,您仍将使用IDLINGRESOURCE。此外,您可以像我们在浓缩咖啡示例中一样替换DI树中的各种物体。
此外,您仍然可以使用Espresso API来测试应用程序与Android框架的集成,例如,用于导航,动画和对话框Windows。
@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()))
}
另外,您可以找到描述ComposeUI tests on Robolectric运行的文章。我个人没有这样做,因为我更喜欢在模拟器外测试UI逻辑。
WireMock / MockWeberver
还有什么可以帮助我们编写测试?可以模拟我们的网络请求的框架。我们已经讨论了该选项,当创建假对象并将其传递到DI树时,我们可以模仿一些业务逻辑并仅测试高级逻辑(演示层)。但是,在某些情况下,进行一次涵盖应用程序所有层的测试仍然很有用。然后,您可以偶然发现问题,例如不稳定的服务器或所需条件的复杂复制。所有这些都使您的测试片状 - 我们已经在上面讨论了这一点。幸运的是,有一些框架使您可以模拟客户端服务器部分。
WireMock 和 MockWeberver 提供类似的功能来替代客户端/服务器交互。让我们以MockWeberver为例。
在进行每个测试之前,我们必须在测试完成后启动服务器并将其停止。通过创建自定义测试圈来做到这一点。
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
}
}
请记住为Raterofit 127.0.0.1
提供测试碱。30。您可以像我们在TestApplicationComponent
中保存假UserRepository
时所涵盖的方式相同的方式。
之后,您可以运行测试,并且,为了响应应用程序的所有请求,您将添加到队列中的响应将返回。请注意,请求的数字和顺序必须严格匹配测试中设置的响应数量。否则,测试肯定会失败。还有一个选项可以使用请求调度器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 具有similar functionality,但是比使用MockWeberver更难使用,而Android社区的支持并不那么强。此外,WireMock具有重要的功能:您可以在记录模式下运行服务器,以便您可以在在线客户端/服务器交互模式下运行测试。 WireMock可以从服务器中写下所有响应并将其保存在文件中。之后,您将可以运行相同的测试,但已经记录了模拟。可以做到这一点,但是 okreplay 非常适合此任务。
加强
使用Okreplay,您可以根据真实服务器请求(类似于WireMock)来准备测试存根。要使用它,您需要在Retrofit/Okhttp测试配置中添加拦截器OkReplayInterceptor
。然后,使用Gradle插件,您可以以记录请求的方式和从服务的响应方式运行测试,并将其响应到.YAML文件中(它们称为磁带)。此外,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上找到许多开源库,实际上,它们包装了浓缩咖啡API,但也有助于解决其一些问题并添加各种令人愉悦的功能。我要告诉你其中两个
咖啡师
咖啡师是浓缩咖啡的额外抽象层,因此与浓缩咖啡相比,它具有多个其他功能。首先,它添加了多种方法,可用于使用UI元素进行更舒适的工作。
例如,而不是原始的浓缩咖啡代码:
@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)
}
该测试已公认变得更加可读。一个缺点是,您需要牢记更多的各种视图器/视图和其他元素,而不是常规意式浓缩咖啡。但是,您仍然可以使用机器人模式使测试更具表现力。您可以了解有关可用方法here的更多信息。咖啡师还提供许多方便的测试规则,例如,用于数据库和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();
但是,您是否应该使用它们或自己写所有内容是一个很好的问题。我们的应用程序通常变得如此复杂,以至于不可能使用标准工具,因此每个开发人员都喜欢编写自己的自定义逻辑。
Kaspresso
另一个由卡巴斯基防病毒开发人员创建的浓缩咖啡包装器。但是,该框架提供的功能远远超过了咖啡师。首先,它使您默认使用页面对象模式编写测试。这是一个不可否认的优势
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的另一个重要功能是,所有视图都包括一些超时处理片状测试,这在我们等待后端的响应的情况下很有用。这可能很方便,但并不是太可靠,因为设置超时有时不够。我建议使用okreplaly或服务器响应模拟更多地依靠iDlingResource和预定义的服务器响应。
此外,Kaspresso还提供了许多其他有用的功能,例如在测试和与Android系统的互动中运行ADB提示。考虑到所有这些都可以在现成的解决方案中获得,Kaspresso是传统浓缩咖啡的绝佳替代品。
结论
像许多其他开发人员一样,我在编写浓缩咖啡测试时遇到了许多困难。测试通常很复杂,缓慢且片状。但是,我们现在有很多库,框架和方法可以大大简化和加快UI测试编写过程。如果我刚才开始使用新应用程序,我将立即与Kaspresso一起编写UI测试。 IdlingResource是背景任务和测试本身同步的必不可少的。如果可能的话,请使用存储库的虚假实现或使用Okreplay记录您的请求和响应。使用页面对象和机器人模式来照顾测试的清洁度和整洁。如果您遵循这些建议,您将能够显着提高测试质量并减少Android App Code中的错误数量。