通过使用UI测试的Android测试来精通开发

大家好。预期将推出一系列有关Android开发基础高级课程,我们准备了一些有趣的材料的翻译。




在Buffer的Android开发团队的去年工作中,我们谈论了很多有关项目纯度和提高项目稳定性的问题。其中一个因素是测试的简介(),我们已经发现它可以帮助我们避免代码中的回归,并使我们对所提供的功能有更多的信心。现在,当我们在Buffer中启动新产品时,我们希望确保在应用新产品时采用相同的方法-只是为了避免出现与以前相同的情况。

背景


在为Buffer应用程序编写单元测试时,我们始终遵循通过测试进行的开发实践(Test-Driven-Development-以下简称TDD)。关于TDD是什么,它的优点以及它们来自何处,有很多资源,但是由于Internet上有很多信息,因此我们将不再赘述。在较高级别上,我个人注意到其中一些:

  • 减少开发时间
  • 更简单,更干净,更可维护的代码
  • 更可靠的代码对我们的工作充满信心。
  • 更高的测试覆盖率(这似乎很明显)

但是直到最近,我们仅以不基于用户界面的实现的单元测试的形式遵循TDD的原则……



我知道,我知道……在实现完成后,我们一直有编写UI测试的习惯-那没有道理。我们监控了TDD的后端,以使我们编写的代码满足测试定义的要求,但是在涉及用户界面测试时,我们编写的测试可以满足特定功能的实现。如您所见,这有点争议,并且在某些方面它是为什么首先使用TDD的原因。

因此,在这里我想考虑为什么会这样,以及我们如何尝试改变。但是,为什么我们首先要注意这一点?

由于我们的应用程序中现有的活动是如何编写的,所以总是很难使用它们。这不是一个完整的借口,但是它的许多依赖性,责任和紧密联系使它们极难测试。对于我们添加的新活动,出于习惯,我本人总是在实现后编写UI测试-除了习惯之外,没有其他原因。但是,在创建模板代码(可用于新项目)时,我想到了一个更改。您将很高兴知道这种习惯已被打破,现在我们正在努力研究TDD进行UI测试

第一步


我们将在这里探讨的是一个非常简单的示例,以便使该概念更易于理解和理解-我希望这足以了解该方法的一些优点。

我们将从创建一个基本活动开始。我们需要这样做,以便我们可以运行UI测试-假设此设置是实现的基础,而不是实现本身。这是我们的基本活动:

class LoginActivity: AppCompatActivity(), LoginContract.View {

    @Inject lateinit var loginPresenter: LoginContract.Presenter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)
    }

    override fun setPresenter(presenter: LoginContract.Presenter) {
        loginPresenter = presenter
    }

    override fun showServerErrorMessage() {
        TODO("not implemented")
    }

    override fun showGeneralErrorMessage() {
        TODO("not implemented")
    }

    override fun showProgress() {
        TODO("not implemented")
    }

    override fun hideProgress() {
        TODO("not implemented")
    }

    override fun showInvalidEmailMessage() {
        TODO("not implemented")
    }

    override fun hideInvalidEmailMessage() {
        TODO("not implemented")
    }

    override fun showInvalidPasswordMessage() {
        TODO("not implemented")
    }

    override fun hideInvalidPasswordMessage() {
        TODO("not implemented")
    }

}

您可能会注意到,此活动除了初始设置外什么都不做,这是该活动所需的。在onCreate()方法中,我们只是设置了一个到布局的链接,我们也有一个到我们的View接口的链接,该接口使用活动来实现,但是还没有实现。

我们在Espresso测试中发现的最常见的事物之一是在我们的应用程序中找到的参考视图和按资源ID的字符串。在这方面,我们再次需要提供一个布局文件以供我们的活动使用。这是由于以下事实:a)我们的活动需要一个布局文件来在测试期间显示布局,并且b)我们需要一个ID视图来查看测试中的链接。让我们继续为登录活动做一个非常简单的布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <EditText
        android:id="@+id/input_email"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <EditText
        android:id="@+id/input_password"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <Button
        android:id="@+id/button_login"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</LinearLayout>

在这里您可以看到,我们并不担心任何样式或位置,请记住-在创建基础而不是实现时。

在设置的最后一部分,我们将定义将在本练习中使用的行。同样,我们将需要在测试中引用它们-直到将它们添加到XML布局或活动类中,只需在file中定义它们即可strings.xml

<string name="error_message_invalid_email”>…</string>
<string name="error_message_invalid_password”>…</string>

您可能会注意到,在此设置中,我们编写的内容尽可能少,但是我们提供了有关活动及其布局的足够详细信息,可以为此编写测试。我们的活动当前不起作用,但是将打开并具有可以引用的视图。现在我们有足够的最低限度可以工作,让我们继续并添加一些测试。

添加测试


因此,我们必须实现三种情况,因此我们将为它们编写一些测试。

  • 当用户在电子邮件输入字段中输入无效的电子邮件地址时,我们需要显示一条错误消息。因此,我们将编写一个测试来检查是否显示此错误消息。
  • 当用户再次开始在电子邮件输入字段中输入数据时,以上错误消息应消失-因此我们将为此编写测试。
  • 最后,当API返回错误消息时,它应该显示在警告对话框中-因此我们还将为此添加测试。

@Test
fun invalidEmailErrorHidesWhenUserTypes() {
    activity.launchActivity(null)

    onView(withId(R.id.button_login))
            .perform(click())

    onView(withId(R.id.input_email))
            .perform(typeText("j"))

    onView(withText(R.string.error_message_invalid_email))
            .check(doesNotExist())
}

@Test
fun invalidPasswordErrorDisplayed() {
    activity.launchActivity(null)

    onView(withId(R.id.button_login))
            .perform(click())

    onView(withText(R.string.error_message_invalid_password))
            .check(matches(isDisplayed()))
}

@Test
fun serverErrorMessageDisplays() {
    val response = ConnectResponseFactory.makeConnectResponseForError()
    stubConnectRepositorySignIn(Single.just(response))
    activity.launchActivity(null)

    onView(withId(R.id.input_email))
            .perform(typeText("joe@example.com"))

    onView(withId(R.id.input_password))
            .perform(typeText(DataFactory.randomUuid()))

    onView(withId(R.id.button_login))
            .perform(click())

    onView(withText(response.message))
            .check(matches(isDisplayed()))
}

好的,现在我们已经编写了测试-让我们继续并运行它们。



而且它们失败了也就不足为奇了-这是因为我们还没有实现,所以应该可以预料到,无论如何,我们应该很高兴看到红色的测试!

因此,现在我们需要为活动添加实现,直到测试通过。当我们编写只测试一个概念(或者至少应该测试!)的重点测试时,我们可以一个接一个地添加实现,还可以看着我们的测试一个又一个地变成绿色。

因此,让我们来看一个失败的测试,让我们从invalidPasswordErrorDisplayed()测试开始。我们知道几件事:

  • , , , , :

private fun setupLoginButtonClickListener() {
    button_login.setOnClickListener {
        loginPresenter.performSignIn(input_email.text.toString(),
                input_password.text.toString()) }
}

  • , . TextInputLayout, , :

override fun showInvalidPasswordMessage() {
    layout_input_password.error = getString(R.string.error_message_invalid_password)
}

现在,我们已经添加了针对这种情况的逻辑,让我们继续并再次运行测试!



很好,看来检查invalidPassworrdErrorDisplays()成功了。但是我们还没有完成,对于必须实现的登录功能的那些部分,我们还有两项测试尚未通过。

接下来,我们将考虑测试serverErrorMessageDisplays()。这非常简单,我们知道当API返回错误响应(而不是来自网络库的一般错误)时,应用程序应在警告对话框中向用户显示错误消息。为此,我们只需要使用对话框文本中的服务器错误消息来创建对话框的实例:

override fun showServerErrorMessage(message: String) {
    DialogFactory.createSimpleInfoDialog(this, R.string.error_message_login_title, message, 
            R.string.error_message_login_ok).show()
}

让我们继续并再次运行测试:



万岁!我们正在前进,现在只剩下一个测试,这是一个测试invalidEmailErrorHidesWhenUserTypes()同样,这是一个简单的情况,但让我们看一下:

  • 当用户单击登录按钮并且电子邮件地址丢失或电子邮件地址不正确时,我们向用户显示一条错误消息。我们已经实现了这一点,为简单起见,我将其排除在外
  • 但是,当用户再次开始在字段中输入数据时,错误消息应从显示中删除。为此,我们需要在输入字段的文本内容更改时进行监听:

private fun setupOnEmailTextChangedListener() {
    input_email.addTextChangedListener(object : TextWatcher {

        override fun afterTextChanged(s: Editable) {}

        override fun beforeTextChanged(s: CharSequence, start: Int,
                                       count: Int, after: Int) {
        }

        override fun onTextChanged(s: CharSequence, start: Int,
                                   before: Int, count: Int) {
            loginPresenter.handleEmailTextChanged(s)
        }
    })
}

现在,这足以确保在更改输入字段的内容时隐藏我们的错误消息。但是我们有测试来确认我们的更改:



太棒了!通过测试时,我们的实施要求已得到满足-看到绿灯很棒

结论


重要的是要注意,我们应用TDD的示例非常原始。假设我们正在开发一个复杂的屏幕,例如内容提要,您可以在其中使用提要元素执行多项操作(例如,在Android的Buffer应用程序中)-在这些情况下,我们将使用应在给定活动中实现的许多不同功能/片段。在这些情况下,UI测试中的TDD会更多地显示出来,因为可以导致我们为这些功能编写太复杂的代码的事实可以简化为满足我们编写的给定测试的实现。

总而言之,我将分享一些从我的经验中学到的要点:

  • , , TDD . , . , Espresso (/, ), . activity, , , . .
  • , , , , , , , . , , , , , ( , !). - . , , , - .
  • , Ui- , , . , , , , , , .
  • , . , — , , , - .
  • 似乎更自然。因为TDD已经用于单元测试,所以在编写单元测试,实现,UI测试时会感到有些倒退。全面尝试TDD,而不是半途而废,您会感到更加自然。

您在编写用户界面测试时是否已经在使用TDD,并且正在做类似或完全不同的事情?还是您想了解更多并问几个问题?随时在下面发表评论或在推特上给我们发消息@bufferdevs

我们正在等待您的课程:


All Articles