友情提示:本篇文章内容较长,预计阅读时间为30分钟。
前序
当我准备研究自动化测试这门技术时,其实我是懵逼的,我无法完全地分清功能测试、UI测试和自动化测试这三者之间的区别和联系,我也不知道自动化测试到底是测试什么和不测试什么?直到在网上看到这么一个关于自动化测试的说法:
把自己当成用户,只关注自己所能看到的东西。
嗯。。。感觉上面那个问题有答案了。
自动化测试这个东西其实就是让机器去模仿人的行为,让它去做整个测试工作。既然是去模仿人的行为,那实际上也应该认为机器只能理解人所能理解的东西。比方说,当我去人为地做一些测试的时候,我所期待的只是UI上的变化可以符合我的预期,至于它背后的数据是怎样的实际上我并不关心。
这个思路的意思是在于,我要让机器模拟我的测试过程,那么我就需要针对那些我(作为用户)能看到的东西,也就是UI。比如说,我并不关心某个网络请求返回值的具体数据是否正确,我关心的是我能在UI上看到我希望看到的结果。基于此,我觉得写各个测试用例的一个通用的思路就是:
找到某个元素,做一些操作,检查结果。
这里包含了三个流程:
- 找元素:找到UI上测试所针对的元素;
- 做操作:给这个元素做一些操作;
- 检查结果:这个元素做出了我期望的行为。
再直观一点,向一个表单输入一段文字,那么整个过程就可以描述为:
- 找元素:找到EditText;
- 做操作:向EditText输入字符串;
- 检查结果:EditText显示了我输入的字符串。
以上三个步骤实际上是我们作为用户在使用一个APP的时候所遵循的流程,而测试也是基本遵循这样一个流程的,各种自动化测试框架也是围绕这三个步骤来提供支持,下面就来说说Espresso这款测试框架。
Espresso
一、简介
Espresso 是 Google 官方提供的一个易于测试 Android UI 的开源框架,于2013年10月推出它的released 版本,目前最新版本已更新到2.x. 并且在AndroidStudio 2.2 预览版中已经默认集成该测试库.
Espresso 由以下三个基础部分组成:
- ViewMatchers - 在当前View层级去匹配指定的View.
- ViewActions - 执行Views的某些行为,如点击事件.
- ViewAssertions - 检查Views的某些状态,如是否显示.
使用Espresso框架的测试代码大致如下:
onView(ViewMatcher) // 1.匹配View
.perform(ViewAction) // 2.执行View行为
.check(ViewAssertion); // 3.验证View
espresso提供了如下图所示的API:

下面就将围绕上图中的这些API来进行讲解。
二、准备
虽然android studio 2.2预览版已经默认集成了espresso,但还是需要再做一些配置。
1. 首先需要在build.gradle的dependencies中增加如下依赖:
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: "com.android.support"
})
androidTestCompile('com.android.support.test:runner:0.5', {
exclude group: "com.android.support"
})
2. 其次需要更改testInstrumentationRunner
android {
...
defaultConfig {
...
testInstrumentationRunner" android.support.test.runner.AndroidJUnitRunner"
...
}
...
}
三、在哪里写
依赖配置好后,就可以开始编写测试用例了!但问题是我们应该在哪里添加测试用例呢?Espresso和其他自动化测试框架不同,在Android Studio中新建一个工程时,在src目录下,和main平级的地方还有一个androidTest目录,一般而言,我们将工程代码放在src/main/java目录下,将与之相关的测试代码放在src/androidTest/java目录下。如下所示:
src/
androidTest/java ----这里存放instrumentation test相关的代码
main/java ----这里存放工程代码
同时,为了让工程更容易维护,建议将相应Class的测试代码放到相同名称的包下面,比如,在Package-name下面有一个Class A:
src/main/java/package-name/A.java
那么,建议将A的测试类放到androidTest下面对应的路径下:
src/androidTest/java/package-name/ATest.java
四、编写一个简单的测试用例
假设有这样一个登录的场景:用户输入手机号和密码后点击登录按钮,登录成功会toast提示"登录成功",用户名或者密码错误导致登录失败会toast提示"用户名或密码错误"。
测试代码为:
@RunWith(AndroidJUnit4.class)
public class LoginActivityTest {
@Rule
public ActivityTestRule<LoginActivity> mRule = new ActivityTestRule<>(LoginActivity.class);
@Test
public void testLogin() {
// 获取手机号的输入框,输入手机号,然后关闭软键盘
onView(withId(R.id.et_mobile)).perform(typeText("17721429141"), closeSoftKeyboard());
// 获取密码的输入框,输入密码,然后关闭软键盘
onView(withId(R.id.et_password)).perform(typeText("123456"), closeSoftKeyboard());
// 点击登录按钮
onView(withId(R.id.btn_login)).perform(click());
// 检查是否有"登录成功"的toast弹出
onView(withText("登录成功")).inRoot(isToast()).check(matches(isDisplayed()));
}
}
代码说明:
- @RunWith这个注解是必需的,定义测试代码会在什么样的环境下运行,@RunWith(AndroidJUnit4.class)就表明是在android的环境下运行。
- @Rule这个注解是定义测试规则的,而ActivityTestRule是用来指定activity的启动规则的,上面的代码表明将LoginActivity作为第一个activity来启动。
- @Test这个注解是定义一个测试用例,当测试代码运行时,所执行的代码就是Test注解下的测试代码。
相应的还有其他一些注解:
- @Before
- @After
- @BeforeClass
- @AfterClass
- @Test(timeout=)
五、查找元素
1.基础查找方法
在上面那个登录的测试用例中,都是用withId()方法来查找UI元素的,espresso也提供了很多查找Ui元素的方法,常用的有:
- withId()
- withText()
- withHint()
- withTagKey()
- withTagValue()
- hasLinks()
- hasFocus() ...
2.组合查找方法
当单一的查找条件不能唯一匹配某一个UI元素时,就可以通过组合多个查找条件来唯一确定某一个UI元素。
比如有这样一个场景:
当需要点击"小炒肉"旁边的添加按钮时,测试代码能像下面这样来写:
onView(allOf(withText("添加"), hasSibling(withText("小炒肉")))).perform(click());
上面这行代码的意思就是:点击 有文本为"小炒肉"的兄弟UI元素 且 自身的文本内容为"添加"的UI元素。
3.自定义查找方法
上面的这些查找方法在平时的实际业务测试中基本上已经够用了,但有时候或许需用通过更加复杂的条件去匹配某个UI元素,而espresso提供的方法又达不到想要的效果,这时自定义查找方法就排上用场了!
下面是一个自定义的匹配方法 - nthChildOf()的例子,它的作用是查找出某个UI元素的第几个子元素:
public static Matcher<View> nthChildOf(final Matcher<View> parentMatcher, final int childPosition) {
return new TypeSafeMatcher<View>() {
@Override
public void describeTo(Description description) {
description.appendText("position " + childPosition + " of parent ");
parentMatcher.describeTo(description);
}
@Override
public boolean matchesSafely(View view) {
if (!(view.getParent() instanceof ViewGroup)) reurn false;
ViewGroup parent = (ViewGroup) view.gtParent();
return parentMatcher.matches(parent)
&& parent.getChildCount() > childPosition
&& parent.getChildAt(childPosition).equals(view);
}
};
}
六、操作元素
找到了目标元素,接下来我们该针对该元素做一些操作了。Espresso提供了如下方法来对相应的元素做操作:
public ViewInteraction perform(final ViewAction... viewActions) {}
该方法定义在ViewInteraction类里面。还记得onView()方法的返回值么?yes,正是一个ViewInteraction对象。因此,我们可以在onView()方法找到的元素上直接调用perform()方法进行一系列操作:
onView(withId(id)).perform(click())
如上代码对onView()查询到的元素做了一次点击的操作。请注意,perform()方法的入参是变长参数,也就意味着,我们可以依次对某个元素做多个操作:
onView(withId(id)).perform(click(), replaceText(text), closeSoftKeyboard())
以上代码对目标元素依次做了点击、输入文本、关闭输入法键盘的操作。这是一个典型的填写表单的行为。
除了上面登录的测试用例中使用的typeText()、closeSoftKeyboard()、click()外,espresso还提供了如下的行为方法:
- doubleClick()
- longClick()
- pressBack()
- pressKey()
- openLink()
- scrollTo()
- swipeLeft()/swipeRight()/swipeUp()/swipeDown()
- clearText()
- replaceText()
七、检查结果
到目前为止,我们已经能找到元素,也能够对元素进行一些操作了!接下来我们需要检查一下这些操作的结果是否符合我们的预期。
Espresso提供了一个check()方法用来检测结果:
public ViewInteraction check(final ViewAssertion viewAssert) {}
该方法接收了一个ViewAssertion的入参,该入参的作用就是检查结果是否符合我们的预期。一般来说,我们可以调用如下的方法来自定义一个ViewAssertion:
public static ViewAssertion matches(final Matcher<? super View> viewMatcher) {}
这个方法接收了一个匹配规则,然后根据这个规则为我们生成了一个ViewAssertion对象!还记得Matcher这个类型么!!是的,这就是onView()方法的入参!实际上他们是同一个类型,其使用方法也是完全一致的。
比如,我想检查一下指定id的TextView是否按照我的预期显示了一段text文本,那么我就可以这样写:
onView(withId(id)).check(matches(withText(text)))
进阶
一、Idling Resource
Espresso官方文档有这样一段话:
Espresso测试有个很强大的地方是它在多个测试操作中是线程安全的。Espresso会等待当前进程的消息队列中的UI事件,并且在任何一个测试操作中会等待其中的AsyncTask结束才会执行下一个测试。这能够解决程序中大部分的线程同步问题。
这句话的意思是Espresso在执行每一个测试操作时会检查下面两个场景:
- 在当前消息队列中没有UI事件;
- 在默认的AsyncTask线程池没有任务;
当这两种情况都满足时才会继续执行测试操作。但是,如果App以其他方式执行长时间运行操作,Espresso不知道如何判断这些操作已经完成。Espresso提供了IdlingResource这个API来达到延时操作的效果,IdlingResource本身是个接口,代码如下:
public interface IdlingResource {
// 用于日志显示的名字,可随意取
public String getName();
// 是否是空闲状态
public boolean isIdleNow();
// 注册变成空闲的回调
public void registerIdleTransitionCallback(ResourceCallback callback);
// 回调接口
public interface ResourceCallback {
public void onTransitionToIdle();
}
}
比如我们在登录界面点击登录按钮后,会在发起网络请求前显示一个LoadingDialog,LoadingDialog是用DialogFragment来实现的,如果我们想要在点击了登录按钮、LoadingDialog消失后再检查某条Toast是否弹出后,就可以通过IdlingResource来实现。
自定义IdlingResource的代码如下:
public class LoadingDialogIdlingResource implements IdlingResource {
private FragmentManager manager;
private String tag;
private ResourceCallback callback;
public LoadingDialogIdlingResource(FragmentManager manager, String tag) {
this.manager = manager;
this.tag = tag;
}
@Override
public String getName() {
return LoadingDialogIdlingResource.class.getSimpleName();
}
@Override
public boolean isIdleNow() {
Fragment fragment = manager.findFragmentByTag(tag);
boolean idle = fragment == null || !(fragment instanceof DialogFragment) || !((DialogFragment) fragment).getDialog().isShowing();
if (idle && callback != null) {
callback.onTransitionToIdle();
}
return idle;
}
@Override
public void registerIdleTransitionCallback(ResourceCallback callback) {
this.callback = callback;
}
}
在测试用例中使用的代码如下:
@RunWith(AndroidJUnit4.class)
public class LoginActivityTest {
@Rule
public ActivityTestRule<LoginActivity> mActivityRule = new ActivityTestRule<>(LoginActivity, true);
private LoadingDialogIdlingResource idlingResource;
@Before
public void setUp() {
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
idlingResource = new LoadingDialogIdlingResource(mActivityRule.getActivity().getSupportFragmentManager(), "TAG");
Espresso.registerIdlingResources();
}
@After
public void tearDown() {
Espresso.unregisterIdlingResources(idlingResource);
idlingResource = null;
}
@Test
public void testLogin() {
// 获取手机号的输入框,输入手机号,然后关闭软键盘
onView(withId(R.id.et_mobile)).perform(typeText("17721429141"), closeSoftKeyboard());
// 获取密码的输入框,输入密码,然后关闭软键盘
onView(withId(R.id.et_password)).perform(typeText("123456"), closeSoftKeyboard());
// 点击登录按钮
onView(withId(R.id.btn_login)).perform(click());
// 在这一步时会等待LoadingDialogIdlingResource的isIdleNow()返回true时才会继续往下执行
// 检查是否有"登录成功"的toast弹出
onView(withText("登录成功")).inRoot(isToast()).check(matches(isDisplayed()));
}
}
二、Intent
在实际的测试业务中,当单独测试某个界面时,或许会遇到这个界面需要它的上一个界面传递数据来显示,如果我们这时候想测试数据有没有显示正确的话。上面的这些基础API就做不到了,这就需要借助Espresso的intent库来支持了。
1.首先需要在build.gradle的dependencies中增加如下依赖:
androidTestCompile('com.android.support.test.espresso:espresso-intents:2.2.2', {
exclude group: "com.android.support"
})
2.替换ActivityTestRule为IntentsTestRule
IntentsTestRule使得在UI功能测试中使用Espresso-Intents API变得简单。该类是 ActivityTestRule的扩展,它会在每一个被 @Test 注解的测试执行前初始化 Espresso-Intents,然后在测试执行完后释放 Espresso-Intents。被启动的 activity 会在每个测试执行完后被终止掉,此规则也适用于 ActivityTestRule。
3.将IntentsTestRule的第三个构造参数设置为false,表明不直接启动activity
@Rule
public IntentsTestRule<ExtractSuccessActivity> mRule = new IntentsTestRule<>(ExtractSuccessActivity.class, true, false);
4.在启动前将需要传递的参数放置到Intent对象中,然后启动activity
@Before
public void setUp() {
Object data = ...;
Intent intent = new Intent();
intent.putExtra("data", data);
mRule.launchActivity(intent);
}
下面以团队版App中提现成功这个页面为例,测试代码如下:
@RunWith(AndroidJUnit4.class)
public class ExtractSuccessActivityTest {
private static final String BANK_NAME = "农业银行";
private static final String CARD_NUM = "6225757538967564";
private static final double AMOUNT = 200;
private static final String EXPECT_AMOUNT = "¥200.00";
private static final String EXPECT_CARD_INFO = "农业银行(7564)";
@Rule
public IntentsTestRule<ExtractSuccessActivity> mRule = new IntentsTestRule<>(ExtractSuccessActivity.class, true, false);
@Before
public void setUp() {
Bank bank = new Bank();
bank.setName(BANK_NAME);
BankCard card = new BankCard();
card.setCardNum(CARD_NUM);
card.setBank(bank);
Intent intent = new Intent();
intent.putExtra("param_bank_card", card);
intent.putExtra("param_amount", AMOUNT);
mRule.launchActivity(intent);
}
@Test
public void testDataShow() {
onView(withId(R.id.txt_extract_amount)).check(matches(withText(EXPECT_AMOUNT)));
onView(withId(R.id.txt_card_info)).check(matches(withText(EXPECT_CARD_INFO)));
}
}
三、跨进程测试
如果有这样一个更改头像的场景:用户点击更改头像按钮后,会调用系统自带的相机进行拍照,然后再回到app提交拍好的照片。在这种场景下,我们需要从自己的app跳转到其他的app,这种跳转在实际业务中是很常见。
Espresso并没有对这种跨app的交互测试提供支持,我们无法在测试代码中通过espresso提供的api获取到非自己app的其他app的UI元素,这时候就需要用到android提供的UI Automator来进行自动化测试了。
需要添加UI Automator的依赖:
dependencies {
...
androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.1'
}
采用UI Automator的过程如下:
1.获得一个UiDevice对象,代表我们正在执行测试的设备。该对象可以通过一个getInstance()方法获取,入参为一个Instrumentation对象:
UiDevice mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
2.通过findObject()方法获取到一个UiObject对象,代表我们需要执行测试的UI组件:
public UiObject findObject(UiSelector selector) {
return new UiObject(this, selector);
}
从如上声明可以看出,findObject()方法接受了一个UiSelector对象,返回了我们需要的UiObject对象。在这里,UiSelector类似于Espresso中的Matcher,也是指定了某种匹配规则,UI Automator会按照UiSelector指定的规则从当前UI上进行控件的查找。不同于Espresso的是,如果找到多个满足规则的控件,则会返回第一个控件。如果没有控件满足当前指定的规则,则会抛出一个UiAutomatorObjectNotFoundException异常。
和Espresso类似,我们可以通过ID、text等属性来进行控件的查找,同时也可以指定目标控件的类型。可以指定一个规则,也可以通过链式调用指定多个规则。比如:
UiObject mCameraSureBtn = mDevice.findObject(new UiSelector().resourceId("com.android.camera:id/v6_btn_done")
.className("android.widget.ImageView"));
这行代码的UiSelector构建就是采用了如下两个组合规则:
- 控件ID为"com.android.camera:id/v6_btn_done",这个ID是从某个MIUI版本系统的系统相机获取的,对应于拍照按钮;
- 控件类型为ImageView。
3.对该UI组件执行一系列操作
UiObject提供了一系列方法用来执行各种各样的操作。比如:
- click():点击控件中心;
- dragTo():拖动控件到指定位置;
- setText():对可输入控件设置文本;
- swipeUp():对控件执行上滑操作。类似地,swipeDown(), swipeLeft()和swipeRight()可以执行相应的操作
4.检查结果
执行一系列操作之后,我们需要对操作的结果进行验证了! 对于结果的验证,我们可以使用之前说到的一系列Assert方法了。比如说,我们需要检测某个控件的文字:
assertEquals(TargetText, mUiObject.getText())
总结
Espresso这个框架整个流程学习下来,发现学习成本还是挺大的,感觉只有懂Android开发的人员才能用Espresso写出测试代码来,不但只支持Android App测试,而且还只能用Java语言来编写,这是一个弊端,但不可否认的是espresso的功能还是蛮强大的,因为其底层是Android API支持的,所以在可扩展性和测试速度方面还是比其他几款自动化测试框架强大不少。所以在选择测试框架时还是需要根据测试的业务来权衡每款测试框架的优缺点,没有哪款框架敢说它什么功能都能满足。