Menguasai pengembangan melalui pengujian di Android menggunakan tes UI

Halo semuanya. Untuk mengantisipasi peluncuran serangkaian kursus dasar dan lanjutan baru tentang pengembangan Android, kami menyiapkan terjemahan materi yang menarik.




Selama tahun terakhir kerja tim pengembangan Android di Buffer, kami berbicara banyak tentang kebersihan proyek kami dan meningkatkan stabilitasnya. Salah satu faktor adalah pengenalan () pengujian, yang, seperti yang telah kami ketahui, membantu kami menghindari regresi dalam kode kami dan memberi kami lebih percaya diri pada fitur yang kami sediakan. Dan sekarang, ketika kami meluncurkan produk baru di Buffer , kami ingin memastikan bahwa pendekatan yang sama diterapkan ketika menyangkut mereka - hanya agar kita tidak berada dalam situasi yang sama seperti sebelumnya.

Latar Belakang


Saat menulis tes unit untuk aplikasi Buffer, kami selalu mematuhi praktik pengembangan melalui pengujian (Test-Driven-Development - selanjutnya TDD). Ada banyak sumber daya tentang apa itu TDD, tentang kelebihannya, dan dari mana asalnya, tetapi kami tidak akan membahas topik ini, karena ada banyak informasi di Internet. Pada level tinggi, saya pribadi mencatat beberapa di antaranya:

  • Mengurangi waktu pengembangan
  • Kode lebih sederhana, lebih bersih, dan lebih bisa dikelola
  • Kode lebih andal dengan lebih percaya diri dalam pekerjaan kami.
  • Cakupan tes yang lebih tinggi (ini terlihat agak jelas)

Tetapi sampai saat ini, kami hanya mengikuti prinsip-prinsip TDD hanya dalam bentuk unit test untuk implementasi kami yang tidak didasarkan pada antarmuka pengguna ...



Saya tahu, saya tahu ... Kami selalu memiliki kebiasaan menulis tes UI setelah apa yang dilaksanakan diselesaikan. - dan itu tidak masuk akal. Kami memantau TDD untuk backend sehingga kode yang kami tulis memenuhi persyaratan yang ditentukan oleh tes, tetapi ketika sampai pada tes antarmuka pengguna, kami menulis tes yang memenuhi penerapan fitur tertentu. Seperti yang Anda lihat, ini agak kontroversial, dan dalam beberapa hal ini adalah tentang mengapa TDD digunakan sejak awal.

Jadi, di sini saya ingin mempertimbangkan mengapa demikian, dan bagaimana kita bereksperimen dengan perubahan. Tapi mengapa kita memperhatikan hal ini sejak awal?

Itu selalu sulit untuk bekerja dengan kegiatan yang ada dalam aplikasi kita karena cara mereka ditulis. Ini bukan alasan yang lengkap, tetapi banyak ketergantungan, tanggung jawab, dan ikatan yang dekat membuat mereka sangat sulit untuk diuji. Untuk kegiatan baru yang kami tambahkan, karena kebiasaan, saya sendiri selalu menulis tes UI setelah implementasi - selain kebiasaan, tidak ada alasan lain untuk ini. Namun, ketika membuat kode templat kami , siap untuk proyek-proyek baru, saya berpikir tentang perubahan. Dan Anda akan senang mengetahui bahwa kebiasaan ini telah rusak, dan sekarang kami sedang mengerjakan sendiri, menjelajahi TDD untuk tes UI

Langkah pertama


Apa yang akan kita jelajahi di sini adalah contoh yang cukup sederhana, sehingga konsepnya lebih mudah diikuti dan dipahami - saya harap ini cukup untuk melihat beberapa keuntungan dari pendekatan ini.

Kita akan mulai dengan membuat aktivitas dasar. Kita perlu melakukan ini agar kita dapat menjalankan tes UI - bayangkan bahwa pengaturan ini adalah dasar untuk implementasi kita, dan bukan implementasi itu sendiri. Seperti apa aktivitas dasar kami:

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")
    }

}

Anda mungkin memperhatikan bahwa aktivitas ini tidak melakukan apa-apa selain pengaturan awal, yang diperlukan untuk aktivitas tersebut. Dalam metode onCreate (), kami cukup mengatur tautan ke tata letak, kami juga memiliki tautan ke antarmuka Tampilan kami, yang diimplementasikan menggunakan aktivitas, tetapi mereka belum memiliki implementasi.

Salah satu hal paling umum yang kami temukan dalam tes Espresso adalah pandangan referensi dan string dengan ID sumber daya yang ditemukan dalam aplikasi kami. Dalam hal ini, kami sekali lagi perlu menyediakan file tata letak untuk digunakan dalam aktivitas kami. Ini disebabkan oleh fakta bahwa: a) aktivitas kami memerlukan file tata letak untuk menampilkan tata letak selama pengujian, dan b) kami memerlukan tampilan ID untuk tautan dalam pengujian kami. Mari kita lanjutkan dan buat tata letak yang sangat sederhana untuk aktivitas login kita:

<?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>

Di sini Anda dapat melihat bahwa kami tidak khawatir tentang gaya atau posisi apa pun, ingat - saat kami membuat fondasi , dan bukan implementasinya.

Dan untuk bagian terakhir dari pengaturan, kita akan mendefinisikan garis-garis yang akan digunakan dalam latihan ini. Sekali lagi, kita perlu referensi mereka dalam tes - sampai Anda menambahkannya ke tata letak XML atau kelas aktivitas Anda, cukup tentukan di file strings.xml.

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

Anda mungkin memperhatikan bahwa dalam pengaturan ini kami menulis sesedikit mungkin, tetapi kami memberikan detail yang cukup tentang aktivitas kami dan tata letaknya untuk menulis tes untuk itu. Aktivitas kami saat ini tidak berfungsi, tetapi terbuka dan memiliki tampilan yang dapat direferensikan. Sekarang kita memiliki cukup minimum untuk bekerja, mari kita lanjutkan dan tambahkan beberapa tes.

Menambahkan Tes


Jadi, kami memiliki tiga situasi yang harus kami laksanakan, jadi kami akan menulis beberapa tes untuk mereka.

  • Ketika pengguna memasukkan alamat email yang tidak valid di bidang input email, kami perlu menampilkan pesan kesalahan. Jadi, kami akan menulis tes yang memeriksa apakah pesan kesalahan ini ditampilkan.
  • Ketika pengguna mulai memasukkan data ke bidang input email lagi, pesan kesalahan di atas akan hilang - karena itu kita akan menulis tes untuk ini.
  • Akhirnya, ketika API mengembalikan pesan kesalahan, itu harus ditampilkan dalam kotak dialog peringatan - jadi kami juga akan menambahkan tes untuk ini.

@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()))
}

Oke, sekarang kami memiliki tes tertulis - mari kita lanjutkan dan jalankan.



Dan tidak mengherankan bahwa mereka gagal - ini karena kami belum memiliki implementasi, jadi ini yang diharapkan. Dalam hal apa pun, kami seharusnya senang melihat tes merah sekarang!

Jadi sekarang kita perlu menambahkan implementasi untuk aktivitas kita sampai tes lulus. Saat kami menulis tes terfokus yang menguji hanya satu konsep (atau setidaknya seharusnya!), Kami akan dapat menambahkan implementasi satu per satu dan juga menyaksikan pengujian kami berubah hijau satu per satu.

Jadi, mari kita lihat salah satu tes yang gagal, mari kita mulai dengan tes invalidPasswordErrorDisplayed () . Kami tahu beberapa hal:

  • , , , , :

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)
}

Sekarang kami telah menambahkan logika untuk situasi ini, mari lanjutkan dan jalankan pengujian kami lagi!



Hebat, sepertinya ceknya invalidPassworrdErrorDisplays()berhasil. Tetapi kami belum selesai, kami masih memiliki dua tes yang belum dilewati untuk bagian-bagian dari fungsi login kami yang harus kami laksanakan.

Selanjutnya, kami akan mempertimbangkan tes serverErrorMessageDisplays(). Ini cukup sederhana, kita tahu bahwa ketika API mengembalikan respons kesalahan (dan bukan kesalahan umum dari perpustakaan jaringan kami), aplikasi akan menampilkan pesan kesalahan kepada pengguna di kotak dialog peringatan. Untuk melakukan ini, kita hanya perlu membuat instance dialog menggunakan pesan kesalahan server kami dalam teks dialog:

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

Mari kita lanjutkan dan jalankan tes kita lagi:



Hore! Kami bergerak maju, sekarang kami hanya memiliki satu tes tersisa, ini adalah tes invalidEmailErrorHidesWhenUserTypes(). Sekali lagi, ini adalah kasus sederhana, tetapi mari kita lihat:

  • Ketika pengguna mengklik tombol login dan alamat email tidak ada atau alamat email salah, kami menunjukkan pesan kesalahan kepada pengguna. Kami sudah menerapkan ini, saya hanya mengesampingkannya untuk kesederhanaan
  • Namun, ketika pengguna mulai memasukkan data ke dalam bidang lagi, pesan kesalahan harus dihapus dari tampilan. Untuk melakukan ini, kita perlu mendengarkan ketika konten teks dari bidang input berubah:

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)
        }
    })
}

Sekarang ini sudah cukup untuk menjamin bahwa pesan kesalahan kami akan disembunyikan ketika mengubah isi kolom input. Tetapi kami memiliki tes untuk mengonfirmasi perubahan kami:



Hebat! Persyaratan implementasi kami terpenuhi saat kami lulus tes kami - senang melihat lampu hijau

Kesimpulan


Penting untuk dicatat bahwa contoh penerapan TDD kami sangat primitif. Bayangkan bahwa kami sedang mengembangkan layar yang kompleks, seperti umpan konten, di mana Anda dapat melakukan beberapa tindakan dengan elemen umpan (misalnya, seperti dalam aplikasi Buffer untuk Android) - dalam kasus ini kami akan menggunakan banyak fungsi berbeda yang harus diimplementasikan dalam aktivitas yang diberikan / pecahan. Ini adalah situasi di mana TDD dalam tes UI akan terungkap lebih banyak, karena apa yang dapat mengarah pada kenyataan bahwa kita akan menulis kode yang terlalu rumit untuk fungsi-fungsi ini dapat dikurangi menjadi implementasi yang memenuhi tes yang diberikan yang kita tulis.

Sebagai rangkuman, saya akan membagikan beberapa poin yang dipelajari dari pengalaman saya:

  • , , TDD . , . , Espresso (/, ), . activity, , , . .
  • , , , , , , , . , , , , , ( , !). - . , , , - .
  • , Ui- , , . , , , , , , .
  • , . , — , , , - .
  • Sepertinya lebih alami. Karena kenyataan bahwa TDD sudah digunakan untuk tes unit, Anda merasa sedikit terbelakang ketika Anda menulis tes unit, diikuti oleh implementasi, diikuti oleh tes UI. Anda akan merasa lebih alami dengan melangkah penuh dengan TDD, bukan di tengah jalan.

Apakah Anda sudah menggunakan TDD saat menulis tes antarmuka pengguna dan melakukan sesuatu yang serupa atau sangat berbeda? Atau Anda ingin tahu lebih banyak dan mengajukan beberapa pertanyaan? Jangan ragu untuk berkomentar di bawah ini atau menciak kami di @bufferdevs

Itu saja. Kami menunggu Anda di kursus:


All Articles