在这篇文章中,您将学习如何使用Espresso测试框架编写UI测试并自动化测试工作流程,而不是使用繁琐且极易出错的手动过程。
Espresso是用于在Android中编写UI测试的测试框架。根据官方文档,您可以:
使用Espresso编写简洁,美观,可靠的Android UI测试。
1. 为什么要使用浓缩咖啡?
手动测试的一个问题是执行起来既费时又乏味。例如,要在Android应用中测试登录屏幕(手动),您必须执行以下操作:
启动应用程序。
导航到登录屏幕。
确认是否
usernameEditText和passwordEditText可见。在各自的字段中键入用户名和密码。
确认登录按钮是否也可见,然后单击该登录按钮。
检查登录成功或失败时是否显示正确的视图。
而不是花费所有这些时间手动测试我们的应用程序,最好花更多时间编写代码,使我们的应用程序脱颖而出!而且,即使手动测试繁琐且速度很慢,它仍然容易出错,您可能会错过一些极端情况。
自动化测试的一些优点包括:
自动化测试每次执行时都会执行完全相同的测试用例。
开发人员可以在将问题发送给QA团队之前快速发现问题。
与手动测试不同,它可以节省大量时间。通过节省时间,软件工程师和QA团队可以将更多时间花在挑战和奖励任务上。
实现更高的测试覆盖率,从而实现更好的应用质量。
在本教程中,我们将通过将Espresso集成到Android Studio项目中来了解Espresso。我们将为登录屏幕和a编写UI测试RecyclerView,我们将学习测试意图。
质量不是一种行为,而是一种习惯。- 巴勃罗毕加索
2. 先决条件
为了能够学习本教程,您需要:
对核心Android API和Kotlin的基本了解
Android Studio 3.1.3或更高版本
Kotlin插件 1.2.51或更高
您可以在我们的GitHub仓库中找到本教程的示例项目(在Kotlin中),以便您轻松跟进。
3. 创建Android Studio项目
启动Android Studio 3并创建一个名为空活动的新项目 MainActivity。一定要检查 包含Kotlin支持。

创建Android项目对话框
4. 设置浓缩咖啡和 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:
一个
[Instrumentation](https://developer.android.com/reference/android/app/Instrumentation.html)运行针对Android包(应用程序)JUnit3和JUnit4测试。
请注意,Instrumentation它只是实现应用程序检测代码的基类。
关闭动画
Espresso的同步,如果您在测试设备上允许动画,则不知道如何等待动画完成,可能导致某些测试失败。要关闭测试设备上的动画,请转到“设置” >“ 开发人员选项”,然后关闭“绘图”部分下的所有以下选项:
窗口动画比例
过渡动画比例
动画师持续时间刻度
5. 在Espresso中写下你的第一个测试
首先,我们开始测试登录屏幕。以下是登录流程的启动方式:用户启动应用程序,显示的第一个屏幕包含一个登录按钮。当登录按钮被点击,它打开了LoginActivity窗口。此屏幕仅包含两个EditTexts(用户名和密码字段)和一个“ 提交”按钮。
这是我们的MainActivity布局:

MainActivity布局
这是我们的LoginActivity布局:

image
我们现在为我们的MainActivity班级写一个测试。转到您的 MainActivity班级,将光标移动到MainActivity名称,然后按Shift-Control-T。在弹出菜单中 选择Create New Test ....

创建新的测试对话框
按OK 按钮,出现另一个对话框。选择androidTest目录, 再次单击“ 确定”按钮。请注意,因为我们正在编写一个检测测试(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.onViewimport 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项目。Hamcrest的匹配库位于我们通过Gradle拉出的Espresso库中。
该onView(withId(R.id.btn_login))会返回一个ViewInteraction是一个ViewID为R.id.btn_login。在上面的示例中,我们用于 withId()查找具有给定id的窗口小部件。我们可以使用的其他视图匹配器是:
withText():返回TextView基于其text属性值匹配的匹配器。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 的匹配器。
要运行测试,可以单击方法旁边的绿色三角形或类名。单击类名旁边的绿色三角形将运行该类中的所有测试方法,而方法旁边的那个将仅针对该方法运行测试。

MainActivityTest类
万岁!我们的测试通过!

Android Studio测试结果工具栏
在视图上执行操作
在ViewInteraction通过调用返回的对象上 onView(),我们可以模拟用户可以在窗口小部件上执行的操作。例如,我们可以通过简单地调用类中的click()静态方法 来模拟单击操作ViewActions。这将为ViewAction我们返回一个 对象。
文档说这 ViewAction 是:
负责在给定的View元素上执行交互。
@Testfun clickLoginButton_opensLoginUi() { // ...
onView(withId(R.id.btn_login)).perform(click())
}我们通过第一次呼叫执行点击事件perform()。此方法对当前视图匹配器选择的视图执行给定操作。请注意,我们可以传递单个操作或操作列表(按顺序执行)。在这里,我们给了它click()。其他可能的行动是:
typeText()模仿键入文本EditText。clearText()模拟清算文本EditText。doubleClick()模拟双击aView。longClick()模仿长按aView。scrollTo()模拟滚动ScrollView到View可见的特定内容。swipeLeft()模拟在a的垂直中心从右向左滑动View。
在ViewActions课堂上可以找到更多的模拟。
使用视图断言进行验证
让我们完成测试,验证LoginActivity每次单击“ 登录”按钮时都会显示屏幕。虽然我们已经看过如何使用check()a ViewInteraction,让我们再次使用它,将它传递给另一个 ViewAssertion。
@Testfun clickLoginButton_opensLoginUi() { // ...
onView(withId(R.id.tv_login)).check(matches(isDisplayed()))
}在LoginActivity布局文件中,除了EditTexts和a之外Button,我们还有一个TextView带ID R.id.tv_login。因此,我们只需进行检查以确认TextView用户是否可见。
现在你可以再次运行测试!

Android Studio测试结果工具栏
如果您正确执行了所有步骤,则测试应成功通过。
这是执行测试过程中发生的事情:
推出了
MainActivity使用该activityRule领域。验证登录按钮(
R.id.btn_login)是否isDisplayed()对用户可见()。模拟
click()该按钮上的单击操作()。验证,如果
LoginActivity通过检查是否显示给用户TextViewid为R.id.tv_login在LoginActivity可见。
您始终可以参考 Espresso备忘单 以查看不同的视图匹配器,查看操作和查看可用的断言。
6. 测试LoginActivity屏幕
这是我们的LoginActivity.kt:
import android.os.Bundleimport android.support.v7.app.AppCompatActivityimport android.widget.Buttonimport android.widget.EditTextimport 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。 在弹出菜单中选择 Create New Test .... 按照与MainActivity.kt相同的过程 ,单击“ 确定”按钮。
import android.support.test.espresso.Espressoimport android.support.test.espresso.Espresso.onViewimport android.support.test.espresso.action.ViewActionsimport android.support.test.espresso.assertion.ViewAssertions.matchesimport android.support.test.espresso.matcher.ViewMatchers.withIdimport android.support.test.espresso.matcher.ViewMatchers.withTextimport android.support.test.rule.ActivityTestRuleimport android.support.test.runner.AndroidJUnit4import org.junit.Ruleimport org.junit.Testimport 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)。它将一直等到View带有id的R.id.tv_login文本读取 成功。
7. 测试a RecyclerView
RecyclerViewActions 是暴露一组API来操作的类 RecyclerView。RecyclerViewActions是工件内部单独工件的一部分espresso-contrib ,也应该添加到build.gradle中:
androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:3.0.2'
请注意,此工件还包含用于UI测试导航抽屉的API DrawerActions 和 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()。我们必须给它一种物品。在我们的例子中,该项目是ViewHolder我们内部的类RandomAdapter。该方法还包含两个参数; 第一个是位置,第二个是动作(ViewActions.click())。
其他RecyclerViewActions可以执行的是:
actionOnHolderItem():ViewAction对匹配的视图执行a viewHolderMatcher。这允许我们通过包含在内部ViewHolder而不是位置中的内容来匹配它。
scrollToPosition():返回ViewAction滚动RecyclerView到某个位置的a。
接下来(一旦“添加注释屏幕”打开),我们将输入注释文本并保存注释。我们无需等待新屏幕打开 - Espresso将自动为我们执行此操作。它等待,直到R.id.add_note_title 找到具有id的视图 。
8. 测试意图
Espresso使用另一个名为espresso-intents测试意图的工件 。这个工件只是Espresso的另一个扩展,专注于Intents的验证和模拟。我们来看一个例子。
首先,我们必须将库拉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
)
}IntentsTestRule延伸ActivityTestRule,所以他们都有类似的行为。以下是该文档所说的内容:
此类是其扩展ActivityTestRule,在每次测试之前初始化Espresso-Intents, Test并在每次测试运行后释放Espresso-Intents。每次测试后活动都将终止,此规则的使用方式与之相同 ActivityTestRule。
主要区别在于它具有额外的功能,可用于测试startActivity()以及startActivityForResult()模拟和存根。
我们现在将测试一个场景,用户将点击R.id.btn_select_contact屏幕上的按钮()从手机的联系人列表中选择一个联系人。
// ...@Testfun 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与应提供存根响应的意图匹配。换句话说, Matcher哪些标识要求您对存根感兴趣。在我们自己的情况下,我们使用hasAction()(辅助方法IntentMatchers)来查找我们的ACTION_PICK请求。然后我们调用respondWith(),设置结果onActivityResult()。在我们的例子中,结果是 Activity.RESULT_OK,模拟用户从列表中选择联系人。
然后我们模拟单击选择联系人按钮,该按钮调用startActivityForResult()。请注意,我们的存根发送了模拟响应onActivityResult()。
最后,我们使用intended()辅助方法简单地验证对调用startActivity()和startActivityForResult()使用正确信息的调用。
作者:Android征途
链接:https://www.jianshu.com/p/7778738889e4
随时随地看视频