在本文中,您将学习如何使用Espresso测试框架编写UI测试以及如何自动化测试工作流程,而不是使用乏味且容易出错的手动过程。
Espresso是用于在Android中编写UI测试的测试框架。 根据官方文档,您可以:
使用Espresso编写简洁,美观且可靠的Android UI测试。
1.为什么要使用意式浓缩咖啡?
手动测试的问题之一是执行起来很耗时且乏味。 例如,要在Android应用中测试登录屏幕(手动),您必须执行以下操作:
- 启动应用程序。
- 导航到登录屏幕。
- 确认
usernameEditText
和passwordEditText
是否可见。 - 在各自的字段中键入用户名和密码。
- 确认登录按钮是否也可见,然后单击该登录按钮。
- 检查该登录成功或失败时是否显示正确的视图。
与其花所有这些时间来手动测试我们的应用程序,不如花更多时间编写使我们的应用程序与众不同的代码! 而且,即使手动测试很繁琐且相当缓慢,它仍然容易出错,并且您可能会错过一些极端情况。
自动化测试的一些优点包括:
- 自动化测试每次执行时都执行完全相同的测试用例。
- 在将问题发送给质量检查团队之前,开发人员可以Swift发现问题。
- 与进行手动测试不同,它可以节省大量时间。 通过节省时间,软件工程师和质量检查团队可以将更多的时间花费在具有挑战性和奖励性的任务上。
- 实现了更高的测试覆盖率,从而带来了更高质量的应用程序。
在本教程中,我们将通过将Espresso集成到Android Studio项目中来对其进行学习。 我们将为登录屏幕和RecyclerView
编写UI测试,并了解测试意图。
质量不是一种行为,而是一种习惯。 - 巴勃罗毕加索
2.先决条件
要遵循本教程,您需要:
- 对核心Android API和Kotlin的基本了解
- Android Studio 3.1.3或更高版本
- Kotlin插件 1.2.51或更高版本
可以在我们的GitHub存储库中找到本教程的示例项目(在Kotlin中),因此您可以轻松地继续学习。
3.创建一个Android Studio项目
启动您的Android Studio 3并创建一个名为MainActivity
的空活动的新项目。 确保选中包括Kotlin支持 。
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包(应用程序)运行JUnit3和JUnit4的Instrumentation
进行测试。
请注意, Instrumentation
只是用于实现应用程序工具代码的基类。
关闭动画
Espresso的同步(不知道如何等待动画完成)会导致某些测试失败-如果您允许在测试设备上播放动画。 要关闭测试设备上的动画,请转到“设置” >“ 开发人员选项” ,然后关闭“绘图”部分下的以下所有选项:
- 窗口动画比例
- 过渡动画比例
- 动画师持续时间量表
5.在Espresso中编写您的第一个测试
首先,我们开始测试登录屏幕。 登录流程的开始过程如下:用户启动应用程序,并且显示的第一个屏幕包含一个“ 登录”按钮。 单击该登录按钮后,它将打开LoginActivity
屏幕。 该屏幕仅包含两个EditText
(用户名和密码字段)和一个Submit按钮。
这是我们的MainActivity
布局:
这是我们的LoginActivity
布局的样子:
现在让我们为MainActivity
类编写一个测试。 转到您的MainActivity
类,将光标移至MainActivity
名称,然后按Shift-Control-T 。 在弹出菜单中选择“ 创建新测试... ”。
按下确定按钮,另一个对话框出现。 选择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的测试。
测试活动
因为我们要测试一个活动,所以我们需要做一些设置。 我们需要在执行之前通知Espresso要打开或启动哪个活动,并在执行任何测试方法后销毁该活动。
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
然后在其后销毁它。
我们还包括@JvmField
Kotlin批注。 这只是指示编译器不要为该属性生成getter和setter,而是将其公开为简单的Java字段。
这是编写Espresso测试的三个主要步骤:
- 查找您要测试的小部件(例如
TextView
或Button
)。 - 在该小部件上执行一项或多项操作。
- 验证或检查该小部件现在是否处于特定状态。
下列类型的注释可以应用于测试类内部使用的方法。
-
@BeforeClass
:这指示此注释所应用的静态方法必须在类中的所有测试之前执行一次。 例如,这可用于建立与数据库的连接。 -
@Before
:指示此注释所附加的方法必须在类中的每个测试方法之前执行。 -
@Test
:指示此注释附加到的方法应作为测试用例运行。 -
@After
:指示此注释所附加的方法应在每个测试方法之后运行。 -
@AfterClass
:指示此注释所附加的方法应在类中的所有测试方法运行之后运行。 在这里,我们通常会关闭@BeforeClass
中打开的资源。
使用onView()
查找View
在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()
的参数类型是Matcher
。 请注意, Matcher
API不是来自Android SDK,而是来自Hamcrest项目 。 Hamcrest的匹配库位于我们通过Gradle拉出的Espresso库中。
onView(withId(R.id.btn_login))
将返回一个ViewInteraction
,它用于ID为R.id.btn_login
的View
。 在上面的示例中,我们使用withId()
查找具有给定id的小部件。 我们可以使用的其他视图匹配器是:
-
withText()
:返回一个匹配器,该匹配器根据其Text属性值匹配TextView
。 -
withHint()
:返回一个根据其提示属性值匹配TextView
的匹配器。
-
withTagKey()
:返回一个基于标记键匹配View
的匹配器。
-
withTagValue()
:返回一个基于标记属性值匹配View
的匹配器。
首先,让我们测试一下按钮是否真正显示在屏幕上。
onView(withId(R.id.btn_login)).check(matches(isDisplayed()))
在这里,我们只是在确认用户是否可以看到具有给定id( R.id.btn_login
)的按钮,因此我们使用check()
方法来确认基础View
是否具有特定状态-在本例中,如果它是可见的。
ViewAssertion
matches()
静态方法返回一个通用的ViewAssertion
,它断言一个视图存在于视图层次结构中,并由给定的视图匹配器进行匹配。 给定的视图匹配器通过调用isDisplayed()
返回。 如方法名称所建议, isDisplayed()
是一个匹配器,用于将当前在屏幕上显示的View
匹配给用户。 例如,如果我们要检查按钮是否已启用,则只需将isEnabled()
传递给matches()
。
我们可以传递给matches()
方法的其他流行的视图匹配器是:
-
hasFocus()
:返回与当前具有焦点的View
匹配的匹配器。 -
isChecked()
:返回一个匹配器,该匹配器仅在视图为CompoundButton
(或其子类型)并且处于选中状态时接受。 此方法的反义是isNotChecked()
。 -
isSelected()
:返回匹配选定View
的匹配器。
要运行测试,可以单击方法或类名旁边的绿色三角形。 单击类名旁边的绿色三角形将运行该类中的所有测试方法,而一个方法旁边的一个仅将针对该方法运行测试。
万岁! 我们的测试通过了!
在视图上执行操作
在通过调用onView()
返回的ViewInteraction
对象上,我们可以模拟用户可以在小部件上执行的动作。 例如,我们可以通过简单地调用ViewActions
类内部的click()
静态方法来模拟click动作。 这将为我们返回一个ViewAction
对象。
该文档说ViewAction
是:
负责在给定的View元素上执行交互。
@Test
fun clickLoginButton_opensLoginUi() {
// ...
onView(withId(R.id.btn_login)).perform(click())
}
我们通过首先调用perform()
执行click事件。 此方法在当前视图匹配器选择的视图上执行给定操作。 请注意,我们可以将单个动作或动作列表(按顺序执行)传递给它。 在这里,我们给了它click()
。 其他可能的操作是:
-
typeText()
模仿将文本键入到EditText
。 -
clearText()
模拟在EditText
清除文本。 -
doubleClick()
模拟双击View
。 -
longClick()
模仿长按View
。 -
scrollTo()
模拟将ScrollView
到可见的特定View
。 -
swipeLeft()
模拟跨View
的垂直中心从右向左滑动。
在ViewActions
类中可以找到更多模拟。
使用视图断言进行验证
让我们完成测试,以验证每当单击“ 登录”按钮时是否显示LoginActivity
屏幕。 尽管我们已经看到了如何在ViewInteraction
上使用check()
,但让我们再次使用它,并将其传递给另一个ViewAssertion
。
@Test
fun clickLoginButton_opensLoginUi() {
// ...
onView(withId(R.id.tv_login)).check(matches(isDisplayed()))
}
在LoginActivity
布局文件中,除了EditText
和一个Button
,我们还有一个ID为R.id.tv_login
的TextView
。 因此,我们只需进行检查以确认TextView
对用户可见。
现在您可以再次运行测试!
如果正确执行所有步骤,则测试应成功通过。
这是执行测试过程中发生的事情:
- 使用
activityRule
字段启动MainActivity
。 - 验证用户是否可以看到登录按钮(
R.id.btn_login
)(isDisplayed()
)。
- 模拟该按钮上的点击动作(
click()
)。 - 验证如果
LoginActivity
通过检查是否显示给用户TextView
id为R.id.tv_login
在LoginActivity
是可见的。
您始终可以参考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相同的过程,然后单击“ 确定”按钮。
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将单击Submit按钮( R.id.btn_submit
)。 它将等待,直到找到ID为R.id.tv_login
的View
且文本读取为Success为止。
7.测试一个RecyclerView
RecyclerViewActions
是公开用于在RecyclerView
上运行的一组API的类。 RecyclerViewActions
是espresso-contrib
构件内部单独构件的一部分,该构件也应添加到build.gradle中 :
androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:3.0.2'
请注意,此工件还包含用于通过DrawerActions
和DrawerMatchers
测试导航抽屉的UI的API。
@RunWith(AndroidJUnit4::class)
class MyListActivityTest {
// ...
@Test
fun clickItem() {
onView(withId(R.id.rv))
.perform(RecyclerViewActions
.actionOnItemAtPosition<RandomAdapter.ViewHolder>(0, ViewActions.click()))
}
}
要单击RecyclerView
中任意位置的项目,我们调用actionOnItemAtPosition()
。 我们必须给它一种物品。 在我们的例子中,该项是我们RandomAdapter
内的ViewHolder
类。 此方法还具有两个参数: 第一个是位置,第二个是动作( ViewActions.click()
)。
可以执行的其他RecyclerViewActions
是:
-
actionOnHolderItem()
:对与viewHolderMatcher
匹配的视图执行ViewAction
。 这使我们可以根据ViewHolder
包含的ViewHolder
而不是位置进行匹配。 -
scrollToPosition()
:返回一个ViewAction
,它将RecyclerView
滚动到某个位置。
接下来(打开“添加注释屏幕”),我们将输入注释文本并保存注释。 我们无需等待新屏幕打开,Espresso将自动为我们完成此操作。 等待直到找到ID为R.id.add_note_title
的视图。
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
)
}
IntentsTestRule
扩展了ActivityTestRule
,因此它们都有相似的行为。 这是医生说的:
此类是ActivityTestRule
的扩展,它在用Test
注释的每个测试之前初始化Espresso-Intents,并在每次测试运行后释放Espresso-Intents。 活动将在每次测试后终止,并且可以与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)))
//...
}
在这里,我们使用espresso-intents
库中的intending()
来为我们的ACTION_PICK
请求设置一个带有模拟响应的存根。 这是当用户单击ID为R.id.btn_select_contact
的按钮来选择联系人时, PickContactActivity.kt中发生的情况。
fun pickContact(v: View)
val i = Intent(Intent.ACTION_PICK,
ContactsContract.Contacts.CONTENT_URI)
startActivityForResult(i, PICK_REQUEST)
}
intending()
接受一个Matcher
,该匹配Matcher
应匹配应为其提供存根响应的意图。 换句话说, Matcher
识别您对存根感兴趣的请求。 在我们自己的情况下,我们利用hasAction()
在一个辅助方法IntentMatchers
)找到我们ACTION_PICK
请求。 然后,我们调用respondWith()
,它为onActivityResult()
设置结果。 在我们的例子中,结果为Activity.RESULT_OK
,模拟用户从列表中选择一个联系人。
然后,我们模拟单击选择联系人按钮,该按钮将调用startActivityForResult()
。 请注意,我们的存根将模拟响应发送到onActivityResult()
。
最后,我们使用intended()
帮助方法来简单地验证对startActivity()
和startActivityForResult()
的调用是使用正确的信息进行的。
结论
在本教程中,您学习了如何在Android Studio项目中轻松使用Espresso测试框架来自动化测试工作流程。
我强烈建议您查阅官方文档,以了解有关使用Espresso编写UI测试的更多信息。
翻译自: https://code.tutsplus.com/tutorials/testing-android-user-interfaces-with-espresso--cms-31687