Kotlin Android UI测试
Kotlin Android UI 测试基础
在 Android 开发中,UI 测试对于确保应用的用户界面功能正常、交互流畅至关重要。Kotlin 作为 Android 开发的首选语言之一,提供了强大的工具和库来进行 UI 测试。
1. 测试框架选择
Android 开发中常用的 UI 测试框架有 Espresso 和 UI Automator。
Espresso:
- 专为 Android 应用的 UI 测试设计,适用于测试应用内的 UI 交互。它能够快速定位视图并执行操作,如点击按钮、输入文本等。
- 优点是简洁易用,测试代码直观,与应用的 UI 紧密结合。
UI Automator:
- 更侧重于跨应用的 UI 测试以及对系统级 UI 的操作。它可以在不同应用之间切换,操作系统级别的控件。
- 优点是功能强大,适用于测试复杂的场景,如多应用交互。
在 Kotlin 中,我们可以方便地使用这两个框架进行 UI 测试。
2. 项目配置
要在 Kotlin 项目中进行 UI 测试,首先需要在 build.gradle
文件中添加相关依赖。
对于 Espresso,在 androidTestImplementation
中添加:
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.4.0'
espresso-core
是核心库,espresso-contrib
则包含了对一些常用 UI 组件(如 RecyclerView)的支持。
对于 UI Automator,添加:
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
同时,确保 androidTest
源集的 AndroidManifest.xml
中配置了正确的权限和 instrumentation 信息。
<instrumentation
android:name="androidx.test.runner.AndroidJUnitRunner"
android:targetPackage="com.example.yourpackage" />
使用 Espresso 进行 UI 测试
1. 定位视图
Espresso 使用 ViewMatchers
来定位视图。例如,要定位一个按钮,可以使用 withId
方法:
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.matcher.ViewMatchers.withId
import org.junit.Test
import com.example.yourpackage.R
class EspressoUITest {
@Test
fun clickButtonTest() {
onView(withId(R.id.button_id)).perform(click())
}
}
这里 R.id.button_id
是按钮在布局文件中的 ID。除了 withId
,还有其他常用的 ViewMatchers
,如 withText
(通过文本内容定位视图)、withClassName
(通过类名定位视图)等。
2. 执行操作
定位到视图后,就可以使用 ViewActions
执行操作。常见的操作有 click()
(点击视图)、typeText()
(输入文本)等。
例如,在一个输入框中输入文本并点击提交按钮:
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.matcher.ViewMatchers.withId
import org.junit.Test
import com.example.yourpackage.R
class EspressoUITest {
@Test
fun inputAndSubmitTest() {
onView(withId(R.id.edit_text_id)).perform(typeText("Hello World"))
onView(withId(R.id.submit_button_id)).perform(click())
}
}
在执行 typeText
操作时,要注意输入框的焦点问题。如果输入框没有焦点,可能需要先调用 ViewActions.click
操作获取焦点。
3. 断言结果
使用 ViewAssertions
可以对 UI 操作的结果进行断言。例如,验证某个视图是否可见:
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import org.junit.Test
import com.example.yourpackage.R
class EspressoUITest {
@Test
fun buttonVisibilityTest() {
onView(withId(R.id.button_id)).perform(click())
onView(withId(R.id.result_view_id)).check(matches(isDisplayed()))
}
}
这里点击按钮后,验证结果视图 result_view_id
是否可见。常用的断言方法还有 matches(withText("expected text"))
(验证视图文本是否符合预期)等。
4. 处理 RecyclerView
在 Android 应用中,RecyclerView 是非常常见的组件。Espresso 提供了 RecyclerViewActions
来处理 RecyclerView。
假设我们有一个 RecyclerView,每个 item 中有一个 TextView 和一个 Button。要点击某个 item 中的按钮,可以这样写:
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition
import androidx.test.espresso.matcher.ViewMatchers.withId
import org.junit.Test
import com.example.yourpackage.R
class RecyclerViewEspressoTest {
@Test
fun clickItemButtonTest() {
onView(withId(R.id.recycler_view_id))
.perform(actionOnItemAtPosition<RecyclerView.ViewHolder>(0, click()))
}
}
这里 0
表示点击 RecyclerView 中第一个 item 的按钮。如果要根据 item 中的文本内容来定位并操作,可以自定义 ViewMatcher
。
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.matcher.BoundedMatcher
import org.hamcrest.Description
import org.hamcrest.Matcher
import android.view.View
import android.widget.TextView
fun withItemText(text: String): Matcher<View> {
return object : BoundedMatcher<View, RecyclerView.ViewHolder>(RecyclerView.ViewHolder::class.java) {
override fun describeTo(description: Description) {
description.appendText("has item text: $text")
}
override fun matchesSafely(holder: RecyclerView.ViewHolder): Boolean {
val view = holder.itemView.findViewById<TextView>(R.id.text_view_in_item)
return view.text.toString() == text
}
}
}
class RecyclerViewEspressoTest {
@Test
fun clickItemButtonByTextTest() {
onView(withId(R.id.recycler_view_id))
.perform(actionOnItem<RecyclerView.ViewHolder>(withItemText("specific text"), click()))
}
}
使用 UI Automator 进行 UI 测试
1. 获取 UiDevice 实例
UI Automator 通过 UiDevice
类来与设备进行交互。首先要获取 UiDevice
实例:
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import org.junit.Before
import org.junit.Test
class UiAutomatorUITest {
private lateinit var device: UiDevice
@Before
fun setup() {
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
}
@Test
fun testAppLaunch() {
device.launchApp("com.example.yourpackage")
}
}
@Before
注解的方法会在每个测试方法执行前调用,确保 UiDevice
实例已经初始化。
2. 定位 UiObject
UI Automator 使用 UiSelector
来定位 UiObject
(代表一个 UI 元素)。例如,定位一个按钮:
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObject2
import org.junit.Before
import org.junit.Test
class UiAutomatorUITest {
private lateinit var device: UiDevice
@Before
fun setup() {
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
}
@Test
fun clickButtonTest() {
val button: UiObject2? = device.findObject(By.res("com.example.yourpackage:id/button_id"))
button?.click()
}
}
这里通过资源 ID 定位按钮。UiSelector
还支持通过文本、类名等多种方式定位。
3. 执行操作和断言
定位到 UiObject
后,可以执行操作并进行断言。例如,输入文本并验证结果:
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObject2
import org.junit.Before
import org.junit.Test
import org.junit.Assert.assertTrue
class UiAutomatorUITest {
private lateinit var device: UiDevice
@Before
fun setup() {
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
}
@Test
fun inputAndVerifyTest() {
val editText: UiObject2? = device.findObject(By.res("com.example.yourpackage:id/edit_text_id"))
editText?.setText("Hello")
val resultText: UiObject2? = device.findObject(By.res("com.example.yourpackage:id/result_text_id"))
assertTrue(resultText?.text == "Hello")
}
}
这里在输入框输入文本后,验证结果文本是否与输入的文本一致。
4. 跨应用操作
UI Automator 的一个强大功能是跨应用操作。例如,从一个应用打开另一个应用,并进行操作:
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObject2
import org.junit.Before
import org.junit.Test
class CrossAppUiAutomatorTest {
private lateinit var device: UiDevice
@Before
fun setup() {
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
}
@Test
fun crossAppTest() {
device.launchApp("com.example.firstpackage")
// 在第一个应用中执行一些操作,比如点击一个按钮打开第二个应用
val openSecondAppButton: UiObject2? = device.findObject(By.res("com.example.firstpackage:id/open_second_app_button_id"))
openSecondAppButton?.click()
device.wait(Until.hasObject(By.pkg("com.example.secondpackage")), 5000)
// 在第二个应用中进行操作,比如点击某个按钮
val secondAppButton: UiObject2? = device.findObject(By.res("com.example.secondpackage:id/second_app_button_id"))
secondAppButton?.click()
}
}
这里先启动第一个应用,通过点击按钮打开第二个应用,然后在第二个应用中进行操作。device.wait
方法用于等待第二个应用启动完成。
高级 UI 测试技巧
1. 处理异步操作
在 Android 应用中,很多操作是异步的,如网络请求、数据库查询等。在 UI 测试中,需要等待异步操作完成后再进行断言。
在 Espresso 中,可以使用 IdlingResource
来处理异步操作。例如,假设我们有一个网络请求,请求完成后会更新 UI:
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import com.example.yourpackage.R
import com.example.yourpackage.ui.MainActivity
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.atomic.AtomicBoolean
@LargeTest
@RunWith(AndroidJUnit4::class)
class AsyncEspressoTest {
private lateinit var activity: MainActivity
private val isLoading = AtomicBoolean(false)
@Before
fun setup() {
activity = launchActivity<MainActivity>()
val idlingResource = object : EspressoIdlingResource {
override val name: String
get() = "network_idling_resource"
override fun isIdleNow(): Boolean {
return!isLoading.get()
}
override fun registerIdleTransitionCallback(callback: IdleCallback) {
}
}
IdlingRegistry.getInstance().register(idlingResource)
}
@After
fun teardown() {
IdlingRegistry.getInstance().unregisterAll()
}
@Test
fun asyncOperationTest() {
onView(withId(R.id.trigger_button_id)).perform(click())
isLoading.set(true)
// 模拟网络请求完成
activity.runOnUiThread {
isLoading.set(false)
}
onView(withId(R.id.result_view_id)).check(matches(isDisplayed()))
}
}
这里定义了一个 IdlingResource
,在异步操作开始时设置 isLoading
为 true
,操作完成后设置为 false
。Espresso 在执行断言前会等待 IdlingResource
变为空闲状态。
在 UI Automator 中,可以使用 UiDevice.wait
方法结合 Until
类来等待异步操作完成。例如:
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObject2
import androidx.test.uiautomator.Until
import org.junit.Before
import org.junit.Test
class AsyncUiAutomatorTest {
private lateinit var device: UiDevice
@Before
fun setup() {
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
}
@Test
fun asyncOperationTest() {
val triggerButton: UiObject2? = device.findObject(By.res("com.example.yourpackage:id/trigger_button_id"))
triggerButton?.click()
device.wait(Until.hasObject(By.res("com.example.yourpackage:id/result_view_id")), 10000)
val resultView: UiObject2? = device.findObject(By.res("com.example.yourpackage:id/result_view_id"))
resultView?.let {
// 进行断言等操作
}
}
}
这里等待结果视图出现,超时时间为 10 秒。
2. 测试不同屏幕尺寸和方向
为了确保应用在不同设备上都能正常显示和交互,需要测试不同的屏幕尺寸和方向。
在 Android 测试中,可以通过 Configuration
类来模拟不同的屏幕尺寸和方向。例如,在 Espresso 测试中:
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import android.content.res.Configuration
import com.example.yourpackage.R
import org.junit.Test
import org.junit.runner.RunWith
@LargeTest
@RunWith(AndroidJUnit4::class)
class ScreenOrientationEspressoTest {
@Test
fun landscapeAndPortraitTest() {
// 竖屏测试
onView(withId(R.id.button_id)).perform(click())
onView(withId(R.id.result_view_id)).check(matches(isDisplayed()))
// 切换到横屏
rotateDevice(Configuration.ORIENTATION_LANDSCAPE)
onView(withId(R.id.button_id)).perform(click())
onView(withId(R.id.result_view_id)).check(matches(isDisplayed()))
// 切换回竖屏
rotateDevice(Configuration.ORIENTATION_PORTRAIT)
}
private fun rotateDevice(orientation: Int) {
val activity = activityScenarioRule.scenario.get()
val config = activity.resources.configuration
config.orientation = orientation
activity.createConfigurationContext(config)
}
}
这里通过 rotateDevice
方法切换屏幕方向,并在不同方向下进行 UI 操作和断言。
在 UI Automator 测试中,同样可以通过 UiDevice.setOrientation
方法来切换屏幕方向:
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObject2
import org.junit.Before
import org.junit.Test
class ScreenOrientationUiAutomatorTest {
private lateinit var device: UiDevice
@Before
fun setup() {
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
}
@Test
fun landscapeAndPortraitTest() {
val button: UiObject2? = device.findObject(By.res("com.example.yourpackage:id/button_id"))
val resultView: UiObject2? = device.findObject(By.res("com.example.yourpackage:id/result_view_id"))
button?.click()
resultView?.let {
// 竖屏断言
}
device.setOrientation(UiDevice.ORIENTATION_LANDSCAPE)
button?.click()
resultView?.let {
// 横屏断言
}
device.setOrientation(UiDevice.ORIENTATION_PORTRAIT)
}
}
3. 集成测试与端到端测试
UI 测试不仅仅局限于单个界面的测试,还可以进行集成测试和端到端测试。
集成测试关注多个组件之间的交互。例如,测试一个 Activity 与一个 Fragment 之间的数据传递和交互:
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import com.example.yourpackage.R
import com.example.yourpackage.ui.MainActivity
import org.junit.Test
import org.junit.runner.RunWith
@LargeTest
@RunWith(AndroidJUnit4::class)
class IntegrationEspressoTest {
@Test
fun activityFragmentInteractionTest() {
launchActivity<MainActivity>()
onView(withId(R.id.trigger_button_in_activity_id)).perform(click())
onView(withId(R.id.result_view_in_fragment_id)).check(matches(isDisplayed()))
}
}
这里点击 Activity 中的按钮,验证 Fragment 中的结果视图是否显示。
端到端测试则模拟用户从启动应用到完成一系列操作的完整流程。例如,测试用户注册登录流程:
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import com.example.yourpackage.R
import com.example.yourpackage.ui.LoginActivity
import org.junit.Test
import org.junit.runner.RunWith
@LargeTest
@RunWith(AndroidJUnit4::class)
class EndToEndEspressoTest {
@Test
fun registerAndLoginTest() {
// 注册流程
launchActivity<RegisterActivity>()
onView(withId(R.id.username_edit_text_id)).perform(typeText("testuser"))
onView(withId(R.id.password_edit_text_id)).perform(typeText("testpass"))
onView(withId(R.id.register_button_id)).perform(click())
// 登录流程
launchActivity<LoginActivity>()
onView(withId(R.id.username_edit_text_id)).perform(typeText("testuser"))
onView(withId(R.id.password_edit_text_id)).perform(typeText("testpass"))
onView(withId(R.id.login_button_id)).perform(click())
// 验证登录成功后的界面
onView(withId(R.id.dashboard_view_id)).check(matches(isDisplayed()))
}
}
这里模拟用户先注册,然后登录,并验证登录后进入的仪表盘视图是否显示。
通过以上对 Kotlin Android UI 测试的详细介绍,从基础框架的使用到高级技巧的应用,希望能帮助开发者更全面、深入地进行 Android 应用的 UI 测试,确保应用的质量和用户体验。在实际开发中,应根据项目的需求和特点,合理选择测试框架和方法,构建完善的 UI 测试体系。