Android 如何实施Jake Wharton';s机器人模式到浓缩咖啡UI测试?

Android 如何实施Jake Wharton';s机器人模式到浓缩咖啡UI测试?,android,android-espresso,android-testing,Android,Android Espresso,Android Testing,Jake Wharton发表了一篇引人入胜的演讲,他提出了一些聪明的方法来改进我们的UI测试,将我们如何在测试中执行UI的细节抽象出来: 他给出的一个示例是如下所示的测试,其中PaymentRobot对象包含如何将付款金额和收件人输入UI的详细信息。放在一个地方很有意义,因此当UI不可避免地发生变化时(例如,重命名字段ID,或从TextEdit切换到TextInputLayout),它只需要在一个地方更新,而不需要整个测试系列。它还使测试更加简洁易读。他建议使用Kotlin使其更加简洁。我不使用

Jake Wharton发表了一篇引人入胜的演讲,他提出了一些聪明的方法来改进我们的UI测试,将我们如何在测试中执行UI的细节抽象出来:

他给出的一个示例是如下所示的测试,其中PaymentRobot对象包含如何将付款金额和收件人输入UI的详细信息。放在一个地方很有意义,因此当UI不可避免地发生变化时(例如,重命名字段ID,或从TextEdit切换到TextInputLayout),它只需要在一个地方更新,而不需要整个测试系列。它还使测试更加简洁易读。他建议使用Kotlin使其更加简洁。我不使用Kotlin,但仍然希望从这种方法中获益

@Test public void singleFundingSourceSuccess {
    PaymentRobot payment = new PaymentRobot();
    ResultRobot result = payment
        .amount(42_00)
        .recipient("foo@bar.com")
        .send();
    result.isSuccess();
}
他概述了Robot类的结构,并给出了一个明确的isSuccess()响应,返回另一个Robot,即下一个屏幕或当前屏幕的状态:

class PaymentRobot {
    PaymentRobot amount(long amount) { ... }
    PaymentRobot recipient(String recipient) { .. }
    ResultRobot send() { ... }
}

class ResultRobot { 
    ResultRobot isSuccess() { ... }
}
我的问题是:

  • Robot如何与活动/片段交互,具体在哪里实例化?我希望在跑步者的测试中会发生这种情况,但他的例子似乎表明情况并非如此。该方法看起来非常有用,但我不知道如何在实践中实现它,无论是针对单个活动/片段还是针对它们的序列
  • 如何扩展此方法,使issucess()方法能够处理各种场景。e、 g.如果我们正在测试登录屏幕,isSuccess()如何处理各种预期结果,例如:身份验证成功、API网络故障和身份验证失败(例如403服务器响应)?理想情况下,API将在改造后进行模拟,并使用端到端UI测试对每个结果进行测试
除了Jake的概述演讲之外,我还没有找到任何实施的例子

机器人如何与活动/片段交互,以及 具体在哪里实例化?我希望这种情况会在未来发生 测试由跑步者进行,但他的例子似乎表明情况并非如此。 这种方法看起来可能非常有用,但我不知道如何使用 在实践中实现它,无论是针对单个活动/片段,还是 一系列的

机器人
应该是用于测试的实用程序类。它不应该是作为
片段
/
活动
或任何您想要使用的内容的一部分包含的生产代码。杰克的类比非常完美。
机器人
的动作就像一个人在与应用程序屏幕交互。因此,无论下面的实现是什么,机器人的公开api都应该是特定于屏幕的。它可以跨越多个
活动
片段
对话框
等,也可以只反映与单个组件的交互。这实际上取决于您的应用程序和测试用例


如何扩展此方法,以便isSuccess()方法可以 处理各种场景。e、 g.如果我们正在测试登录屏幕,如何 isSuccess()能否处理各种预期结果,如:身份验证 成功、API网络故障和身份验证失败(例如403服务器 答复)?理想情况下,API将在改造后被模拟,并且每个 使用端到端UI测试测试结果

您的
机器人的API实际上应该指定测试中的内容。而不是如何。因此,从使用
机器人的角度来看,它不会在意API网络故障还是身份验证故障。这是如何做到的。“你怎么会以失败告终?”。人类QA测试人员(
==Robot
)不会通过查看http流来注意api失败和http超时之间的区别。他/她只会看到您的屏幕显示
故障
机器人
只关心
成功
失败

您可能需要在此处测试的另一件事是,您的应用程序是否显示了一条消息,通知用户连接错误(无论确切原因如何)


我完全误解了意式浓缩咖啡的工作原理,这导致我对如何将其应用于页面对象模式产生了更多的困惑。我现在看到,Espresso不需要任何类型的引用来引用测试中的活动,只是在runner规则的上下文中运行。对于其他任何挣扎的人,这里有一个充实的示例,将robot/page对象模式应用于登录屏幕上的验证测试,其中有一个用户名和密码字段,我们正在测试其中一个字段为空时是否显示错误消息:

java(用于抽象自动化登录活动)

(请注意,构造函数正在测试以确保当前屏幕是我们期望的屏幕)

LoginValidationTests.java:

@LargeTest
@RunWith(AndroidJUnit4.class)
public class LoginValidationTests {

    @Rule
    public ActivityTestRule<LoginActivity> mActivityTestRule = new ActivityTestRule<>(LoginActivity.class);

    @Test
    public void loginPasswordValidationTest() {
        LoginRobot loginPage = new LoginRobot();
        loginPage.enterPassword("");
        loginPage.enterUsername("123");
        loginPage.clickLogin();
        onView(withText(R.string.login_bad_password))
 .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)));
    }

    @Test
    public void loginUsernameValidationTest() {
        LoginRobot loginPage = new LoginRobot();
        loginPage.enterUsername("");
        loginPage.enterPassword("123");
        loginPage.clickLogin();
        onView(withText(R.string.login_bad_username)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)));
    }

}
@LargeTest
@RunWith(AndroidJUnit4.class)
公共类LoginValidationTests{
@统治
public ActivityTestRule mActivityTestRule=新的ActivityTestRule(LoginActivity.class);
@试验
public void loginPasswordValidationTest(){
LoginRobot loginPage=新的LoginRobot();
登录页面。输入密码(“”);
登录页面。输入用户名(“123”);
loginPage.clickLogin();
onView(带文本(R.string.login\u bad\u password))
.检查(匹配(使用有效可见性(ViewMatchers.Visibility.Visibility));
}
@试验
public void loginUsernameValidationTest(){
LoginRobot loginPage=新的LoginRobot();
登录页面。输入用户名(“”);
登录页面。输入密码(“123”);
loginPage.clickLogin();
onView(使用文本(R.string.login\u bad\u username))。检查(匹配项(使用有效可见性(ViewMatchers.Visibility.Visibility)));
}
}
像这样抽象出自动化UI的机制可以避免跨测试的大量重复,也意味着更改的可能性更小
result.isFailure().signalsConnectionError();
public class LoginRobot {

    public LoginRobot() {
        onView(withId(R.id.username)).check(matches(isDisplayed()));
    }

    public void enterUsername(String username) {
        onView(withId(R.id.username)).perform(replaceText(username));
    }

    public void enterPassword(String password) {
        onView(withId(R.id.password)).perform(replaceText(password));
    }

    public void clickLogin() {
        onView(withId(R.id.login_button)).perform(click());
    }

}
@LargeTest
@RunWith(AndroidJUnit4.class)
public class LoginValidationTests {

    @Rule
    public ActivityTestRule<LoginActivity> mActivityTestRule = new ActivityTestRule<>(LoginActivity.class);

    @Test
    public void loginPasswordValidationTest() {
        LoginRobot loginPage = new LoginRobot();
        loginPage.enterPassword("");
        loginPage.enterUsername("123");
        loginPage.clickLogin();
        onView(withText(R.string.login_bad_password))
 .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)));
    }

    @Test
    public void loginUsernameValidationTest() {
        LoginRobot loginPage = new LoginRobot();
        loginPage.enterUsername("");
        loginPage.enterPassword("123");
        loginPage.clickLogin();
        onView(withText(R.string.login_bad_username)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)));
    }

}