AS 自动化测试

本文主要讲解 ATSL 测试框架

JUnit 3 vs Junit 4

AndroidJUnitRunner是一个可以用来运行JUnit 3和JUnit 4样式的测试类的Test Runner,并且同时支持Espresso和UI Automator。这是对于之前的InstrumentationTestRunner的一个升级,如果你去查看Gradle文档中对于Testing配置的说明,会发现推荐的Test Runner为InstrumentationTestRunner。InstrumentationTestRunner只支持JUnit 3样式的测试用例,而我们在写Android测试用例时应该尽可能使用JUnit 4样式来实现。

相对于Junit 3, JUnit 4有如下改进:

  1. 在JUnit3中需要继承TestCase类,但在JUnit4中已经不需要继承TestCase
  2. 可以使用类似@Test, @Before, @After等注解来管理自己的测试方法。在JUnit3中需要覆盖TestCase中的setUp和tearDown方法,其中setUp方法会在测试执行前被调用以完成初始化工作,而tearDown方法则在结束测试结果时被调用,用于释放测试使用中的资源,而在JUnit4中,只需要在方法前加上@Before,@After
  3. 测试方法名不再需要以test开头。在JUnit3中对某个方法进行测试时,测试方法的命令是固定的,例如对addBook这个方法进行测试,需要编写名字为tetAddBook的测试方法,而在JUnit4中没有方法命令的约束,在方法的前面加上@Test,这就代表这个方法是测试用例中的测试方法
  4. 新的断言assertThat
  5. @BeforeClass 和 @AfterClass 。在JUnit3,如果所有的test case仅调用一次setUp()和tearDown()需要使用TestSetup类
  6. 测试异常处理@Test(expected = DataFormatException.class)
  7. 设置超时@Test(timeout = 1000)
  8. 忽略测试@Ignore
  9. 支持对assert方法的static导入。
  10. 增加了一些Assert方法;

使用 Junit 4 风格的 Espresso APIs 来编写的测试用例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RunWith(AndroidJUnit4.class)
@LargeTest
public class MainActivityInstrumentationTest {
@Rule
public ActivityTestRule mActivityRule = new ActivityTestRule<>(
MainActivity.class);
@Test
public void sayHello(){
onView(withText("Say hello!")).perform(click());
onView(withId(R.id.textView)).check(matches(withText("Hello, World!")));
}
}

Junit 4支持以下注解来管理测试用例
  • @Before: 标识在运行测试方法之前运行的代码。可以支持同一个Class中有多个@Before,但是这些方法的执行顺序是随机的。该注解替代了JUnit 3中的setUp()方法。
  • @After: 标识在运行测试方法结束之后运行的代码。可以在其中做一些释放资源的操作。该注解替代了JUnit 3中的tearDown()方法。
  • @Test: 标识一个测试方法。一个测试类中可以有多个测试方法,每个测试方法需要用一个@Test注解来标识。
  • @Rule: 简单来说,是为各个测试方法提供一些支持。具体来说,比如我需要测试一个Activity,那么我可以在@Rule注解下面采用一个ActivityTestRule,该类提供了对相应Activity的功能测试的支持。该类可以在@Before和@Test标识的方法执行之前确保将Activity运行起来,并且在所有@Test和@After方法执行结束之后将Activity杀死。在整个测试期间,每个测试方法都可以直接对相应Activity进行修改和访问。
  • @BeforeClass: 为测试类标识一个static方法,在测试之前只执行一次。
  • @AfterClass: 为测试类标识一个static方法,在所有测试方法结束之后只执行一次。
  • @Test(timeout=): 为测试方法设定超时时间。

区别

  • Espresso 侧重于应用内测试 + 不支持 WebView
  • UiAutomator 支持 WebView,应用内测试也ok,但是 api 的优雅性不如 Espresso

Espresso 自动化测试

Google 新退出的自动化测试框架,除了不支持 WebView,其它的测试功能都能很好的支持。

获取 Context

Espresso 中获取 Context 是通过

1
2
3
4
@Rule
public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(
MainActivity.class);
Context context = mActivityRule.getActivity().getApplicationContext();

测试用例是工程强相关的,上面的 MainActivity 测试工程的主 UI,ActivityTestRule 这个类提供了相应的 Activity 测试支持。

常规 View 测试

例子如下:

1
2
3
4
5
6
@Test
public void changeText_sameActivity() {
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)));
}

  • ViewMachers

    withId: 使用控件 ID 匹配一个 [Matcher],具体的api见文档
    withText: 使用 String 返回一个 Matcher

    1
    2
    public static Matcher<View> withId(final int id);
    public static Matcher<View> withText(String text);
  • onView: 使用一个 Matcher 查找一个 ViewInteraction

    1
    2
    public static ViewInteraction onView(final Matcher<View> viewMatcher);
    public ViewInteraction check(final ViewAssertion viewAssert); // 配合 matches 来做校验操作
  • ViewActions 操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public static ViewAction click() // 点击事件
    public static ViewAction typeText(String stringToBeTyped) // 输入事件
    public static ViewAction closeSoftKeyboard() // 关闭键盘
    public ViewInteraction perform(final ViewAction... viewActions) // 执行一系列的 viewActions 操作
    // typeText 输入text
    // closeSoftKeyboard() 关闭软键盘
    onView(withId(R.id.editText))
    .perform(typeText(mValidStringToBeTyped), closeSoftKeyboard())
    .check(matches(withText(mValidStringToBeTyped)));

测试工程 vs 主工程

在写测试用例的过程,除了能通过 onView 获取到主工程中的 View 控件,这是一个 ViewInteraction 类型,它暴露的 api 接口有限,无法通过它来获取控件的内容,这个时候就需要其它方法了;比如我们需要获取 主工程中 TextView 或者 EditText 的内容等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 此处入参是 String 类型,返回的是一个 Matcher
static Matcher<View> withHint(final String substring) {
return withHint(is(substring));
}
static Matcher<View> withHint(final Matcher<String> stringMatcher) {
checkNotNull(stringMatcher);
return new BoundedMatcher<View, EditText>(EditText.class) {
@Override
public boolean matchesSafely(EditText view) {
final CharSequence hint = view.getHint();
return hint != null && stringMatcher.matches(hint.toString());
}
@Override
public void describeTo(Description description) {
description.appendText("with hint: ");
stringMatcher.describeTo(description);
}
};
}

通过 Matcher,结合 check、matches 来一起达到验证的目的。
使用方法如下:

1
onView(withId(R.id.editText)).check(matches(HintMatcher.withHint(hintText)));

ListView Adapter 测试

  • View 滑动
    这里只需要弄清楚一个 List 的滑动接口就好。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    private static DataInteraction onRow(String str){
    // 滑动到value=str指定的item中
    // LongListActivity.ROW_TEXT 这个属性是在创建 Adapter 时对应子 View 的一个 id,后面的值是对应的 id 赋值
    DataInteraction dataInteraction = onData(hasEntry(equalTo(LongListActivity.ROW_TEXT), is(str)));
    try {
    sleep(3000); // 这里添加参数只是为了方便演示
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    return dataInteraction;
    // return onData(hasEntry(equalTo(LongListActivity.ROW_TEXT), is(str)));
    }
  • 子 View 的点击

    1
    2
    3
    4
    // 滑到 TEXT_ITEM_30 这一行 点击相应的 id 操作
    onRow(TEXT_ITEM_30).onChildView(withId(R.id.rowToggleButton)).perform(click());
    // 滑到 TEXT_ITEM_30 这一行 找到相应的 View,判断 ToggleButton 点击态
    onRow(TEXT_ITEM_30).onChildView(withId(R.id.rowToggleButton)).check(matches(isChecked()));

异步测试

实际测试过程中,对于比较耗时的操作进行操作,需要考虑异步问题,这里可以采用测试工程通过主动 sleep 来解决,但是这个操作本身不够友好,此时可以通过 IdlingResource 来处理。
这个接口类说明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface IdlingResource {
//用于日志显示的名字,可随意取
public String getName();
//是否是空闲状态
public boolean isIdleNow();
//注册变成空闲的回调
public void registerIdleTransitionCallback(ResourceCallback callback);
//回调接口
public interface ResourceCallback {
public void onTransitionToIdle();
}
}

这里可以给一个简单的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class SimpleIdlingResource implements IdlingResource {
@Nullable private volatile ResourceCallback mCallback;
// Idleness is controlled with this boolean.
private AtomicBoolean mIsIdleNow = new AtomicBoolean(true);
@Override
public String getName() {
return this.getClass().getName();
}
@Override
public boolean isIdleNow() {
return mIsIdleNow.get();
}
@Override
public void registerIdleTransitionCallback(ResourceCallback callback) {
mCallback = callback;
}
/**
* Sets the new idle state, if isIdleNow is true, it pings the {@link ResourceCallback}.
* @param isIdleNow false if there are pending operations, true if idle.
*/
public void setIdleState(boolean isIdleNow) {
mIsIdleNow.set(isIdleNow);
if (isIdleNow && mCallback != null) {
mCallback.··();
}
}

通过 IdlingResource 来完成异步测试需要三步:

  1. 在主工程中创建一个 IdlingResource,并暴露一个获取实例的方法供测试工程调用,可以使用注解 @VisibleForTesting

    1
    2
    3
    4
    5
    6
    7
    8
    @VisibleForTesting
    @NonNull
    public IdlingResource getIdlingResource() {
    if (mIdlingResource == null) {
    mIdlingResource = new SimpleIdlingResource();
    }
    return mIdlingResource;
    }
  2. 在测试工程中调用上方法,将 IdlingResource 注册到系统中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 注册
    @Before
    public void registerIdlingResource() {
    mIdlingResource = mActivityRule.getActivity().getIdlingResource();
    // To prove that the test fails, omit this call:
    Espresso.registerIdlingResources(mIdlingResource);
    }
    // 反注册
    @After
    public void unregisterIdlingResource() {
    if (mIdlingResource != null) {
    Espresso.unregisterIdlingResources(mIdlingResource);
    }
    }
  3. 在主工程中调用下面两个方法完成耗时操作的标记

    1
    2
    idlingResource.setIdleState(false); // 来阻塞测试线程
    idlingResource.setIdleState(true); // 来放开测试线程

假设我们使用 processMessage 方法来处理一个耗时操作,内部使用handler,下面是示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static void processMessage(final String message, final DelayerCallback callback,
@Nullable final SimpleIdlingResource idlingResource) {
// The IdlingResource is null in production.
if (idlingResource != null) {
idlingResource.setIdleState(false);
}
// Delay the execution, return message via callback.
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
if (callback != null) {
callback.onDone(message);
if (idlingResource != null) {
idlingResource.setIdleState(true);
}
}
}
}, DELAY_MILLIS);
}

Intent相关测试

这里会作为一个专题重点来讲

  • Base
  • Advance
    比如 startActivityForResult 这种类型的跳转测试

多窗口测试

Android 系统允许多个 View 控件位于系统的最顶层目录上,只是层级不一样,这样会造成有的 View 控件被遮挡导致不可交互,那测试工程通过什么手段能知道当前是哪些 View 控件处理可交互的状态,哪些是不可交到状态呢。

UiAutomator 自动化测试

这个提供了 Android 手机上应用间的自动化测试,它可以很方便的模拟人在使用手机过来中出现的各种操作,并提供了相关的验证方法;
UiAutomator 与 Espresso 框架有一个不同的地方在于获取 Context。

InstrumentationRegistry

这是一个暴露的注册实例,持有 instrumentation 运行的进程和参数,还提供了一种简便的方法调用 instrumentation, application context 和 instrumentation 参数。
它主要用来获取 Context 及 Instrumentation 实例。

注意:自动化测试代码 跟 Android 工程代码实际上没有什么区别,所以在有 Context 的情况下,能做的事情很多,比如关闭打开 Wifi,启动 Activity 应用等等。

1
2
InstrumentationRegistry.getInstrumentation(); // 可用于获取 UiDevice
InstrumentationRegistry.getContext(); // 获取 Context

UiDevice

这个是 uiautomator 提供的一个操作设备的类,它可以模拟各种设备相关的操作,比如按击Home键,菜单栏等,它包含了大量的 API,具体可以参考上面的文档。

1
UiDevice mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());

By + BySelector + Until

这是很实用的一组工具类,三者结合可以用来完成查找或修改及验证(Until类)功能,上面附有对应的文档,可以查阅API,下面介绍一下简单的使用:

1
2
Until.hasObject(By.pkg(pkgName).depth(0)) // 判断 pkgName app 是否在当前手机最上层
Until.findObject(By.res(pkgName, "textToBeChanged")) // 查找 pkgName 应用中 id 为 textToBeChanged 的组件

wait 判断

测试用例中用来的超时等待,比如我要判断当前某一个应用当前是否在最上层,并且可以设置超时时间,这个 wait的还会返回一个 Object 实体。

1
2
mDevice.wait(Until.hasObject(By.pkg(launcherPackage).depth(0)), 5000);
mDevice.wait(Until.hasObject(By.pkg(pkgName).depth(0)), 5000);

下面这个理解一下,wait 操作返回一个 Object,然后获取这个 Object 的内容

该操作相比 mDevice.findObject,只是多加了一个超时等待

1
2
3
4
UiObject2 changedText = mDevice.wait(
Until.findObject(By.res(BASIC_SAMPLE_PACKAGE, "textToBeChanged")),
500 );
assertThat(changedText.getText(), is(equalTo("UiAutomator")));

findObject

该接口返回一个实体组件,然后再调用对应的方法,入参为包句,及 id

1
2
3
4
mDevice.findObject(By.res(BASIC_SAMPLE_PACKAGE, "editTextUserInput"))
.setText("UiAutomator");
mDevice.findObject(By.res(BASIC_SAMPLE_PACKAGE, "activityChangeTextBtn"))
.click();

assertThat 验证

1
2
assertThat(mDevice, notNullValue());
assertThat(changedText.getText(), is(equalTo("UiAutomator")));

check 方法

这里罗列一些常用的check方法:

1
2
3
4
check(matches(isDisplayed())); // 检测是否显示状态(可用于 TextView)
check(matches(not(isDisplayed()))); // 检测是否非显示状态
check(doesNotExist()); // 检测是否可见在当前屏
check(matches(isChecked())); // 检测 ToggleButton 是否点击

anyof 接口

1
2
3
4
5
6
7
8
9
10
onView(withId(R.id.editText)).check(
matches(
HintMatcher.withHint(
anyOf(
endsWith(COFFEE_ENDING),
endsWith(COFFEE_INVALID_ENDING)
)
)
)
);

Espresso cheat sheet

引用