UI测试对于我们是一个熟悉又陌生的字眼,我们能从很多渠道听到这个字眼,都在说测试的自动化是多么多么的重要,但是无论我们是查看了官方文档,还是看了一圈博客回来,都很难对我们的现有的项目去展开,有时为了测试甚至要更改原有框架,许多的不便让我们望而却步,但是如果你走出第一步,就会发现其实并没有想象中的那么难,等你走完,更会发觉这个东西是如此的重要能让我们的开发效率得到不小的提升,对于一个团队的标准化更是有质的提升。
1. 搭建UI测试框架
在Android的自动化UI测试中「Espresso」能完成我们大部分的需求,我们的测试流程也是围绕Espresso展开的。
由于Google现在在逐步将各种支持库都转移到AndroidX中,我们的讲解也会涉及到两个库的不同实现,同时我们以AndroidX为主来讲解,只是引入方式稍有不同,使用起来大同小异。
首先,引入Espresso库
// 可以放在根.gradle里控制版本,这里为了紧凑写在了一起。
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:core:1.2.0-alpha03'
androidTestImplementation 'androidx.test.ext:truth:1.2.0-alpha03'
androidTestImplementation 'androidx.test.ext:junit:1.1.1-alpha03'
androidTestImplementation 'androidx.test:runner:1.2.0-alpha03'
androidTestImplementation 'androidx.test:rules:1.2.0-alpha03'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0-alpha03'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.2.0-alpha03'
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.2.0-alpha03'
// testImplementation 'junit:junit:4.12'
// androidTestImplementation 'com.android.support.test:runner:1.0.2'
// androidTestImplementation 'com.android.support.test:rules:1.0.2'
// androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
// androidTestImplementation 'com.android.support.test.espresso:espresso-intents:3.0.2'
// androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:3.0.2'//recyclerView
同时在项目build.gradle中加入
defaultConfig {
//...
//如果使用androidx
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
//如果使用support
//testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
我们引入这些lib之后,就可以开始编写测试用例了,但是你可能会遇到一些问题。
Test running failed: Instrumentation run failed due to 'Process crashed.'
如果遇到这个错误,一般是我们的引入的包不一致或者testInstrumentationRunner与引入包不一致。注意:如果我们的项目中还在使用Support包中的内容,比如RecyclerView,必须要使用Support包,因为在测试包中引用的都是相同路径下的控件,比如androidx.test.espresso:espresso-contrib包中引用的就是androidx下的RecyclerView,如果混用是会报错的。
其他解决方案
2. 测试入门
大家可以先想象一下,我们在测试一个UI的时候流程是什么样的。1.打开界面 2.找到相应的控件 3.点击控件 4.查看效果
倘若让你设计一个UI测试框架你会怎么设计呢?想必我们的思路也是相似的,只不过是使用代码控制,我们先来编写一个简单的例子来感受一下吧。
basic
我们输入文字,点击change text按钮,上面的文字就可以变为输入的文字。
依照处理流程,我们写出测试代码。
@RunWith(AndroidJUnit4.class)
@LargeTest
public class BasicActivityTest {
public static final String STRING_TO_BE_TYPED = "Hello";
@Rule
public ActivityScenarioRule<BasicTestActivity> activityScenarioRule
= new ActivityScenarioRule<>(BasicTestActivity.class);
@Test
public void changeText_sameActivity() {
onView(withId(R.id.editTextUserInput)).perform(click());
// 输入文字,注意:这里输入法通常会有问题,建议使用Google输入法或者直接使用模拟器,或者把系统设置为英文
onView(withId(R.id.editTextUserInput))
.perform(typeText(STRING_TO_BE_TYPED), closeSoftKeyboard());
// 点击按钮
onView(withId(R.id.changeTextBt)).perform(click());
// 检查是否与输入相符
onView(withId(R.id.textToBeChanged)).check(matches(withText(STRING_TO_BE_TYPED)));
}
}
我们已经完成了一个用例编写,运行之后它会自动键入文字,并且点击按钮,最后检查是否与输入相同。下面我们具体介绍一下这些代码的含义,让你自己也能写出同样的测试用例。
解析
- 首先我们先看下最开始的两个注解,当一个类使用@RunWith注解类时,JUnit将调用它引用的类来运行该类中的测试而不是内置于JUnit中的运行器,这里引用了AndroidJUnit4的运行器,是一个用于Android测试的跨环境JUnit4运行器,这里我们不深究,有兴趣的可以研究一下,我们可以理解为是一种Android的测试环境。
@LargeTest 其实是一种规定,相对的还有@SmallTest和@MediumTest
test类型 | 运行时长 | 使用限制 | 测试场景 |
---|---|---|---|
SmallTest | <200ms | 网络、文件、数据不能使用 | 运行在一个孤立的环境,用于调用非常频繁的场景,多是单元测试 |
MediumTest | <1000ms | 可以通过定义的接口访问文件、数据库、ContentProviders,但是不能访问网络,长时间的阻塞操作应该直接使用Mock | 多用于单个组件的测试 |
LargeTest | >1000ms | 基本无限制 | 用于测试应用组件,UI测试 |
-
@Rule 这个规则提供单个活动的功能测试,通过这个注解可以自动调起目标Activity。
@Test 就是标记的要测试的流程。 -
测试中我们很明显能看到几个关键方法,而这些方法,都遵循着下图的的三个步骤。
测试流程
- onView() 就是找到要测试的View,里面既可以通过标准的id(withId),也可以通过变更的text(withText)
- perform() 就是所有操作的总称,可以是click、longClick、scrollTo等等,我们一般需要的都能找到。
- check() 就是所有检验方法的总称,通常借助Matcher来进行结果的校验,比如字符显示正确与否、View是否显示、View是否处于正确的位置等等。
怎么样?是不是很简单的样子,依靠这个我们可以把应用中的点逐个击破,在开发和重构中事半功倍。
进阶
通常在我们学习完TextView、Button等等普通View之后,我们就要学习一些复杂的View,这个View往往就是RecyclerView。在RecyclerView中我们不能以普通的思路用id去寻找某个View,但是作为应用中非常重要的一部分,Google已经为我们想好了解决方案,就是espresso-contrib包,里面封装了大部分RecyclerView的操作,我们可以以平常使用的思维来使用这些API。
@RunWith(AndroidJUnit4.class)
@LargeTest
public class RecyclerViewTest {
//如果使用Support包,需要替换为ActivityTestRule
@Rule
public ActivityScenarioRule<MainActivity> activityScenarioRule
= new ActivityScenarioRule<>(MainActivity.class);
private void openActivity() {
onView(withId(R.id.button_test_recyclerView)).perform(click());
}
@Test
public void clickPhone(){
openActivity();
onView(withId(R.id.recycler_view_test)).perform(
RecyclerViewActions.actionOnItemAtPosition(0, click()));
}
}
使用RecyclerViewActions中的方法,我们可以很容易的操作RecyclerView,比如可以通过Scroll来滑动到某个位置,通过actionOnItemAtPosition来获取Item Layout进而进行点击操作。
但是你会发现一个问题,如果点击的事件不是这个Item而是Item中的一个View呢?
如果查看源码的话你会发现所有的perform方法中都是一个ViewAction,而这个ViewAction是一个接口,我们可以查看ViewAction的众多实现,其实都是做了特别简单的操作,由此我们可以自定一个ViewAction来实现我们的需求。
public class RecyclerViewActionItem implements ViewAction {
private int mViewId;
public RecyclerViewActionItem(int mViewId) {
this.mViewId = mViewId;
}
@Override
public Matcher<View> getConstraints() {
return null;
}
@Override
public String getDescription() {
return "Click on a child view with specified id.";
}
@Override
public void perform(UiController uiController, View view) {
view.findViewById(mViewId).performClick();
}
public static ViewAction clickRecyclerChildWithId(@IdRes int viewId) {
return new RecyclerViewActionItem(viewId);
}
}
在我们的实现中仅仅是在perform方法中,使用findViewById()找到View,继而调用View的performClick(),在实际的操作中,只要我们知道了具体的View类型,其实还可以有更多的操作。
RecyclerViewTest
延时操作
在一个普通的App中,网络的请求可谓是不可或缺的,而这也为我们的UI自动化测试设下了一个阻拦,我们的测试都是代码一行一行的执行,如果没有相应的页面显示,就会报错失败,(对了,通常在UI测试中,我们会在开发者选项中,把动画都关闭,避免动画赶不上变化)这也是我们在写测试用例时比较麻烦的一个地方。
Google给出了几个方案
- 调用Thread.sleep()
- 通过一个无限循环去获取数据
- 使用CountDownLatch
但是这几个方案都有问题,就是不稳定,效率也不高,如果网络特别差的话,有可能直接报错,于是Google给出了espresso-idling-resource的解决方案。
在gradle中加入(注意不是androidTestImplementation,而是implementation)
implementation 'androidx.test.espresso:espresso-idling-resource:3.2.0-alpha03'
自定义一个IdlingResource
public class SimpleIdlingResource implements IdlingResource {
@Nullable
private volatile ResourceCallback mCallback;
// Idleness is controlled with this boolean.
private AtomicBoolean mIsIdleNow = new AtomicBoolean(true);
//用来标识 IdlingResource 名称
@Override
public String getName() {
return this.getClass().getName();
}
//当前 IdlingResource 是否空闲
@Override
public boolean isIdleNow() {
return mIsIdleNow.get();
}
//注册一个空闲状态变换的ResourceCallback回调
@Override
public void registerIdleTransitionCallback(ResourceCallback callback) {
mCallback = callback;
}
/**
* 设置idle的状态,如果成功请求到了数据将isIdleNow设为true
*/
public void setIdleState(boolean isIdleNow) {
mIsIdleNow.set(isIdleNow);
if (isIdleNow && mCallback != null) {
mCallback.onTransitionToIdle();
}
}
}
需要改变一些原有的代码
handler.postDelayed(new Runnable() {
@Override
public void run() {
if (callback != null) {
callback.onDone(message);
if (idlingResource != null) {
idlingResource.setIdleState(true);
}
}
}
}, DELAY_MILLIS);
在用例中也需要特殊的处理
@RunWith(AndroidJUnit4.class)
@LargeTest
public class ChangeTextBehaviorTest {
private static final String STRING_TO_BE_TYPED = "Espresso";
private IdlingResource mIdlingResource;
@Rule
public ActivityScenarioRule<IdlingTestActivity> mActivityRule = new ActivityScenarioRule<>(
IdlingTestActivity.class);
/**
* 注册mIdlingResource
*/
@Before
public void registerIdlingResource() {
mActivityRule.getScenario().onActivity(new ActivityScenario.ActivityAction<IdlingTestActivity>() {
@Override
public void perform(IdlingTestActivity activity) {
mIdlingResource = activity.getIdlingResource();
// To prove that the test fails, omit this call:
IdlingRegistry.getInstance().register(mIdlingResource);
}
});
}
@Test
public void changeText_sameActivity() {
// Type text and then press the button.
onView(withId(R.id.editTextUserInput))
.perform(typeText(STRING_TO_BE_TYPED), closeSoftKeyboard());
onView(withId(R.id.changeTextBt)).perform(click());
// Check that the text was changed.
onView(withId(R.id.textToBeChanged)).check(matches(withText(STRING_TO_BE_TYPED)));
}
@After
public void unregisterIdlingResource() {
if (mIdlingResource != null) {
IdlingRegistry.getInstance().unregister(mIdlingResource);
}
}
}
具体例子可以查看官方
我们可以看出这个测试其实是有侵入性的,但是这也是加入一些必要功能要做出的取舍,Google给出的方案其实是在IdlingResource的基础上改造架构,但这付出的努力恐怕还是要远远多于Thread.sleep()。
如果有更好的方案,希望大家提出来一起讨论。
总结
无论是单元测试还是UI测试,在我们没有使用的时候似乎觉得可有可无,总是听到很多大牛在说它的重要性,但是却无法感受到它的重要程度,但是当你开始使用之后就会发现,如果一个项目拥有了自动化的测试,你的代码将得到一个提升,因为它迫使你去做封装和改造,让项目去更优雅的测试;添加新功能之后也可以免除可能会对旧功能的后顾之忧;提升了你和测试同学的效率;最后,你拥有了重构的底气,因为,所有的功能都是一样的,但是代码变的更健壮了,UI变的更流畅了,你的技术也变的更好了。
所以,开始你的『测试之旅』吧。
网友评论