在这篇文章中,您将了解如何使用 Espresso 测试框架编写 UI 测试并自动化您的测试工作流程,而不是使用繁琐且极易出错的手动过程。
Espresso 是一个用于在 android 中编写 UI 测试的测试框架。根据官方文档,您可以:
使用 Espresso 编写简洁、美观、可靠的 Android UI 测试。
1. 为什么要使用Espresso?
手动测试的问题之一是执行起来既费时又乏味。例如,要在 Android 应用程序中(手动)测试登录屏幕,您必须执行以下操作:
启动应用程序。
导航到登录屏幕。
确认usernameEditTextand是否passwordEditText可见。
在各自的字段中输入用户名和密码。
确认登录按钮是否也可见,然后单击该登录按钮。
检查登录成功或失败时是否显示正确的视图。
与其花所有时间手动测试我们的应用程序,不如花更多时间编写使我们的应用程序脱颖而出的代码!而且,即使手动测试很乏味且速度很慢,它仍然容易出错,并且您可能会错过一些极端情况。
自动化测试的一些优点包括:
自动化测试每次执行时都会执行完全相同的测试用例。
开发人员可以在将问题发送给 QA 团队之前快速发现问题。
与手动测试不同,它可以节省大量时间。通过节省时间,软件工程师和 QA 团队可以将更多时间花在具有挑战性和奖励性的任务上。
实现了更高的测试覆盖率,从而带来更好的应用质量。
在本教程中,我们将通过将 Espresso 集成到 Android Studio 项目中来了解它。我们将为登录屏幕和 a 编写 UI 测试recyclerview,并且我们将了解测试意图。
质量不是一种行为,而是一种习惯。- 巴勃罗毕加索
2. 先决条件
为了能够遵循本教程,您需要:
可以在我们的 GitHub 存储库中找到本教程的示例项目(在 Kotlin 中), 以便您轻松跟进。
3. 创建一个Android Studio项目
启动您的 Android Studio 3 并创建一个新项目,其中包含一个名为 MainActivity. 确保选中 Include Kotlin support。
4. 设置 Espresso 和 AndroidJUnitRunner
创建新项目后,请确保在 build.gradle中添加来自 Android 测试支持库的以下依赖项(尽管 Android Studio 已经为我们包含了它们)。在本教程中,我们使用的是最新的 Espresso 库版本 3.0.2(在撰写本文时)。
android { //... defaultConfig { //... testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } //... } dependencies { //... androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test:rules:1.0.2' }
我们还包括了仪表运行器AndroidJUnitRunner:
针对 Android 包(应用 Instrumentation 程序)运行 JUnit3 和 JUnit4 测试的 。
请注意,这Instrumentation只是用于实现应用程序检测代码的基类。
关闭动画
Espresso 的同步不知道如何等待动画完成,如果您允许在测试设备上播放动画,可能会导致某些测试失败。要关闭测试设备上的动画,请转到设置 >开发人员选项并关闭“绘图”部分下的所有以下选项:
窗口动画比例
过渡动画比例
动画师持续时间比例
5. 用 Espresso 编写你的第一个测试
首先,我们开始测试登录屏幕。下面是登录流程的开始方式:用户启动应用程序,显示的第一个屏幕包含一个登录按钮。单击该登录按钮时,它会打开LoginActivity屏幕。此屏幕仅包含两个EditTexts(用户名和密码字段)和一个提交按钮。
这是我们的MainActivity布局的样子:
这是我们的LoginActivity布局的样子:
现在让我们为我们的MainActivity班级编写一个测试。转到您的 MainActivity班级,将光标移动到MainActivity名称上,然后按Shift-Control-T。在弹出菜单中 选择创建新测试...。
按OK 按钮,将显示另一个对话框。选择androidTest目录并 再次单击OK按钮。请注意,因为我们正在编写仪器测试(Android SDK 特定测试),所以测试用例位于androidTest/java文件夹中。
现在,Android Studio 已经成功为我们创建了一个测试类。在类名之上,包括这个注解: @RunWith(AndroidJUnit4::class).
import android.support.test.runner.AndroidJUnit4 import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class MainActivityTest { }
该注解表示该类中的所有测试都是 Android 特定的测试。
测试活动
因为我们要测试一个 Activity,所以我们必须做一些设置。我们需要通知 Espresso 在执行之前打开或启动哪个 Activity,并在执行任何测试方法之后销毁。
import android.support.test.rule.ActivityTestRule import android.support.test.runner.AndroidJUnit4 import org.junit.Rule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class MainActivityTest { @Rule @JvmField var activityRule = ActivityTestRule<MainActivity>( MainActivity::class.java ) }
请注意,@Rule注释意味着这是一个 JUnit4 测试规则。JUnit4 测试规则在每个测试方法之前和之后运行(用 注释 @Test)。在我们自己的场景中,我们希望 MainActivity在每个测试方法之前启动并在之后销毁它。
我们还包括了@JvmFieldKotlin 注释。这只是指示编译器不要为属性生成 getter 和 setter,而是将其公开为简单的 Java 字段。
以下是编写 Espresso 测试的三个主要步骤:
查找要测试的小部件(例如 TextView或Button)。
对该小部件执行一项或多项操作。
验证或检查该小部件现在是否处于某种状态。
以下类型的注释可以应用于测试类内部使用的方法。
@BeforeClass:这表示应用此注释的静态方法必须在类中的所有测试之前执行一次。例如,这可以用于建立与数据库的连接。
@Before: 表示此注解所附加的方法必须在类中的每个测试方法之前执行。
@Test: 表示这个注解附加到的方法应该作为测试用例运行。
@After: 表示这个注解附加到的方法应该在每个测试方法之后运行。
@AfterClass: 表示这个注解所附加的方法应该在类中的所有测试方法都运行之后运行。在这里,我们通常会关闭在@BeforeClass.
查找View使用onView()
在我们的MainActivity布局文件中,我们只有一个小部件——Login按钮。让我们测试一个用户会找到该按钮并单击它的场景。
import android.support.test.espresso.Espresso.onView import android.support.test.espresso.matcher.ViewMatchers.withId // ... @RunWith(AndroidJUnit4::class) class MainActivityTest { // ... @Test @Throws(Exception::class) fun clickLoginButton_opensLoginUi() { onView(withId(R.id.btn_login)) } }
要在 Espresso 中查找小部件,我们使用onView()静态方法(而不是findViewById())。我们提供的参数类型onView()是 a Matcher。请注意,MatcherAPI 并非来自 Android SDK,而是来自Hamcrest Project。Hamcrest 的匹配器库位于我们通过 Gradle 提取的 Espresso 库中。
将onView(withId(R.id.btn_login))返回一个IDViewInteraction为 的 a 。在上面的示例中,我们过去常常 寻找具有给定 id 的小部件。我们可以使用的其他视图匹配器是: ViewR.id.btn_loginwithId()
withText():返回一个匹配器,TextView根据其文本属性值进行匹配。
withHint():返回一个匹配器,TextView根据其提示属性值进行匹配。
withTagKey()View:返回一个基于标签键匹配的匹配器。
withTagValue()View:根据标签属性值返回匹配 s 的匹配器。
首先,让我们测试一下按钮是否实际显示在屏幕上。
onView(withId(R.id.btn_login)).check(matches(isDisplayed()))
在这里,我们只是确认具有给定 id ( R.id.btn_login) 的按钮是否对用户可见,因此我们使用该check()方法来确认底层View是否具有某种状态——在我们的例子中,如果它是可见的。
matches()静态方法返回一个泛型,ViewAssertion它断言视图存在于视图层次结构中并由给定的视图匹配器匹配。通过调用返回给定的视图匹配器 isDisplayed()。正如方法名称所暗示的,isDisplayed() 是一个匹配器,View将当前显示在屏幕上的 s 匹配给用户。例如,如果我们想检查一个按钮是否启用,我们只需传递isEnabled() 给 matches().
matches()我们可以传递给该方法的其他流行的视图匹配器是:
hasFocus():返回匹配 View当前具有焦点的 s 的匹配器。
isChecked():返回一个匹配器,当且仅当视图是一个CompoundButton (或子类型)并且处于选中状态时才接受。这种方法的反面是isNotChecked()。
isSelected(): 返回一个匹配View被选中的 s 的匹配器。
要运行测试,您可以单击方法或类名旁边的绿色三角形。单击类名旁边的绿色三角形将运行该类中的所有测试方法,而方法旁边的将只运行该方法的测试。
万岁!我们的测试通过了!
对视图执行操作
在ViewInteraction调用返回的对象上 onView(),我们可以模拟用户可以在小部件上执行的操作。例如,我们可以通过简单地调用类中的click()静态方法 来模拟点击动作ViewActions。这将为我们返回一个 ViewAction对象。
文档说 ViewAction 是:
负责对给定的 View 元素执行交互。
@Test fun clickLoginButton_opensLoginUi() { // ... onView(withId(R.id.btn_login)).perform(click()) }
我们通过首先调用来执行点击事件perform()。此方法对当前视图匹配器选择的视图执行给定的操作。请注意,我们可以将单个操作或操作列表(按顺序执行)传递给它。在这里,我们给了它click()。其他可能的行动是:
typeText() 模仿在EditText.
clearText() 模拟清除文本中的EditText.
doubleClick() 模拟双击 a View。
longClick() 模仿长按 a View。
scrollTo() 模拟滚动ScrollView到特定View的可见。
swipeLeft() 模拟在 a 的垂直中心从右向左滑动View。
更多的模拟可以在ViewActions类中找到。
带有视图断言的有效日期
让我们完成我们的测试,以验证LoginActivity每次单击登录按钮时都会显示屏幕。虽然我们已经看到了如何check()在 a 上ViewInteraction使用,让我们再次使用它,将它传递给另一个 ViewAssertion.
@Test fun clickLoginButton_opensLoginUi() { // ... onView(withId(R.id.tv_login)).check(matches(isDisplayed())) }
在LoginActivity布局文件中,除了EditTexts 和 a Button,我们还有一个TextViewwith ID R.id.tv_login。因此,我们只需进行检查以确认TextView用户可见。
现在您可以再次运行测试!
如果您正确执行了所有步骤,您的测试应该会成功通过。
以下是在执行我们的测试过程中发生的事情:
启动了 MainActivity 使用activityRule领域。
验证登录按钮 ( R.id.btn_login) 是否对用户可见 ( isDisplayed())。
模拟click()该按钮上的单击操作 ( )。
通过检查id中的a是否可见来验证是否LoginActivity向用户显示。TextViewR.id.tv_loginLoginActivity
您可以随时参考 Espresso 备忘单 以查看可用的不同视图匹配器、视图操作和视图断言。
6. 测试LoginActivity屏幕
这是我们的LoginActivity.kt:
import android.os.Bundle import android.support.v7.app.AppCompatActivity import android.widget.Button import android.widget.EditText import android.widget.TextView class LoginActivity : AppCompatActivity() { private lateinit var usernameEditText: EditText private lateinit var loginTitleTextView: TextView private lateinit var passwordEditText: EditText private lateinit var submitButton: Button override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_login) usernameEditText = findViewById(R.id.et_username) passwordEditText = findViewById(R.id.et_password) submitButton = findViewById(R.id.btn_submit) loginTitleTextView = findViewById(R.id.tv_login) submitButton.setOnClickListener { if (usernameEditText.text.toString() == "chike" && passwordEditText.text.toString() == "password") { loginTitleTextView.text = "Success" } else { loginTitleTextView.text = "Failure" } } } }
在上面的代码中,如果输入的用户名是“chike”,密码是“password”,则登录成功。对于任何其他输入,它都是失败的。现在让我们为此编写一个 Espresso 测试!
转到 LoginActivity.kt,将光标移动到 LoginActivity 名称上,然后按 Shift-Control-T。 在弹出菜单中选择 创建新测试...。遵循与MainActivity.kt相同的过程 ,然后单击OK按钮。
import android.support.test.espresso.Espresso import android.support.test.espresso.Espresso.onView import android.support.test.espresso.action.ViewActions import android.support.test.espresso.assertion.ViewAssertions.matches import android.support.test.espresso.matcher.ViewMatchers.withId import android.support.test.espresso.matcher.ViewMatchers.withText import android.support.test.rule.ActivityTestRule import android.support.test.runner.AndroidJUnit4 import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class LoginActivityTest { @Rule @JvmField var activityRule = ActivityTestRule<LoginActivity>( LoginActivity::class.java ) private val username = "chike" private val password = "password" @Test fun clickLoginButton_opensLoginUi() { onView(withId(R.id.et_username)).perform(ViewActions.typeText(username)) onView(withId(R.id.et_password)).perform(ViewActions.typeText(password)) onView(withId(R.id.btn_submit)).perform(ViewActions.scrollTo(), ViewActions.click()) Espresso.onView(withId(R.id.tv_login)) .check(matches(withText("Success"))) } }
这个测试类与我们的第一个非常相似。如果我们运行测试,我们的LoginActivity屏幕就会打开。用户名和密码分别输入到R.id.et_username和R.id.et_password字段中。接下来,Espresso 将单击提交按钮 ( R.id.btn_submit)。它将等到可以找到带有 Viewid的文本读取Success。 R.id.tv_login
7. 测试一个 RecyclerView
RecyclerViewActions 是公开一组 API 以在 RecyclerView. RecyclerViewActions是工件内单独工件的一部分espresso-contrib ,也应添加到build.gradle:
androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:3.0.2'
请注意,此工件还包含用于通过 DrawerActions 和 测试抽屉式导航的 UI 的 API DrawerMatchers。
@RunWith(AndroidJUnit4::class) class MyListActivityTest { // ... @Test fun clickItem() { onView(withId(R.id.rv)) .perform(RecyclerViewActions .actionOnItemAtposition<RandomAdapter.ViewHolder>(0, ViewActions.click())) } }
要单击 a 中任何位置的项目RecyclerView,我们调用actionOnItemAtPosition(). 我们必须给它一个类型的项目。在我们的例子中,item 是ViewHolder我们的. 该方法还接受两个参数;第一个是位置,第二个是动作()。 RandomAdapterViewActions.click()
其他RecyclerViewActions可以执行的有:
actionOnHolderItem()ViewAction:对匹配的视图执行 a viewHolderMatcher。这允许我们通过包含在里面的内容ViewHolder而不是位置来匹配它。
scrollToPosition(): 返回ViewAction滚动RecyclerView到某个位置的 a。
接下来(一旦“添加注释屏幕”打开),我们将输入注释文本并保存注释。我们无需等待新屏幕打开 - Espresso 会自动为我们完成此操作。它一直等到 R.id.add_note_title 可以找到具有 id 的视图。
8. 测试意图
Espresso 使用了另一个以 espresso-intents测试意图命名的工件。这个工件只是 Espresso 的另一个扩展,专注于 Intent 的验证和模拟。让我们看一个例子。
首先,我们必须将espresso-intents库拉入我们的项目中。
androidTestImplementation 'com.android.support.test.espresso:espresso-intents:3.0.2'
import android.support.test.espresso.intent.rule.IntentsTestRule import android.support.test.runner.AndroidJUnit4 import org.junit.Rule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class PickContactActivityTest { @Rule @JvmField var intentRule = IntentsTestRule<PickContactActivity>( PickContactActivity::class.java ) }
IntentsTestRuleextends ActivityTestRule,所以它们都有相似的行为。这是文档所说的:
此类是 的扩展 ActivityTestRule,它在每次使用注释的测试之前初始化 Espresso-Intents, Test并在每次测试运行后释放 Espresso-Intents。Activity 将在每次测试后终止,此规则的使用方式与 ActivityTestRule.
主要区别在于它具有用于测试startActivity()以及startActivityForResult()模拟和存根的附加功能。
我们现在要测试一个场景,用户单击R.id.btn_select_contact屏幕上的按钮 ( ) 以从手机的联系人列表中选择一个联系人。
// ... @Test fun stubPick() { var result = Instrumentation.ActivityResult(Activity.RESULT_OK, Intent(null, ContactsContract.Contacts.CONTENT_URI)) intending(hasAction(Intent.ACTION_PICK)).respondWith(result) onView(withId(R.id.btn_select_contact)).perform(click()) intended(allOf( toPackage("com.google.android.contacts"), hasAction(Intent.ACTION_PICK), hasdata(ContactsContract.Contacts.CONTENT_URI))) //... }
在这里,我们intending()从库中使用为我们的请求espresso-intents设置一个带有模拟响应的存根。ACTION_PICK这是 当用户单击带有 id 的按钮 来选择联系人时PickContactActivity.kt中发生的情况。R.id.btn_select_contact
fun pickContact(v: View) val i = Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI) startActivityForResult(i, PICK_REQUEST) }
intending()接受Matcher与应提供存根响应的意图相匹配的 a。换句话说, Matcher标识您对存根感兴趣的请求。在我们自己的例子中,我们使用hasAction()(a helper method in IntentMatchers) 来查找我们的ACTION_PICK请求。然后我们调用respondWith(),它将结果设置为onActivityResult()。在我们的例子中,结果有 Activity.RESULT_OK,模拟用户从列表中选择一个联系人。
然后我们模拟单击选择联系人按钮,该按钮调用startActivityForResult(). 请注意,我们的存根将模拟响应发送到onActivityResult().
最后,我们使用helper 方法来简单地intended()验证调用是否使用了正确的信息。 startActivity()startActivityForResult()
结论
在本教程中,您学习了如何在 Android Studio 项目中轻松使用 Espresso 测试框架来自动化您的测试工作流程。
- 关闭动画
- 测试活动
- 查找View使用onView()
- 对视图执行操作
- 带有视图断言的有效日期