Khám phá Test-Driven-Development với Android UI Testes
This post hasn't been updated for 3 years
Getting started
Chúng ta sẽ bắt đầu bằng cách tạo ra một activity barebone. Chúng ta cần làm điều này để có thể chạy UI test - hãy nghĩ đến thiết lập này làm nền tảng cho việc thực hiện của chúng ta chứ không phải là bản thân quá trình triển khai thực hiện. Dưới đây là barbone activity:
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")
}
}
Bạn sẽ nhận thấy rằng activity này không làm gì ngoại trừ thiết lập ban đầu mà một activity yêu cầu. Trong method onCreate (), chúng ta chỉ set layout reference, chúng ta cũng có tham chiếu đến ui View interface mà activity đã implement - nhưng bạn sẽ nhận thấy rằng những điều này không có implement nào được nêu ra.
Một trong những điều phổ biến nhất chúng ta làm trong các test Espresso là các tham chiếu và chuỗi tham chiếu bởi các resource ID được tìm thấy trong ứng dụng. Vì lý do này, chúng ta cần phải cung cấp file layout barebone cho activity. Điều này là do a) activity cần một file layout để hiển thị bố cục trong quá trình test và b) chúng ta cần xem ID để tham khảo trong các test. Chúng ta hãy tiếp tục và ta một layout đơn giản cho activity đăng nhập:
<?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>
Bạn sẽ nhận thấy rằng chúng ta đã không làm phiền với bất kỳ styling hoặc position, chúng ta đang tạo ra một nền tảng ,không phải là một implementation.
Và trong phần cuối cùng của quá trình thiết lập, chúng ta sẽ tiếp tục và xác định bất kỳ chuỗi nào sẽ được sử dụng trong activity này. Một lần nữa, chúng ta sẽ cần phải tham khảo những điều này trong các test - đừng thêm chúng vào layout xml hoặc class activity, chỉ cần xác định chúng trong tệp strings.xml.
<string name="error_message_invalid_email”>…</string>
<string name="error_message_invalid_password”>…</string>
Bạn sẽ thấy rằng từ thiết lập này, chúng ta viết ít nhất có thể, nhưng cung cấp đủ chi tiết trong activity và layout của nó để viết test. Activity không hoạt động vào thời điểm này, nhưng nó mở ra và có quan điểm tham khảo. Bây giờ chúng tôi có đủ thứ để làm việci, hãy tiếp tục và thêm một số test.
Thêm các test
Vì vậy, có ba tình huống mà chúng ta biết cần implement, vì vậy hãy viết một số test cho nó.
- Khi người dùng không nhập địa chỉ email hợp lệ vào trường nhập email, chúng ta cần hiển thị và thông báo lỗi. Vì vậy, chúng ta sẽ viết một test để kiểm tra xem thông báo lỗi này có được hiển thị hay không.
- Khi người dùng bắt đầu nhập vào trường nhập email, thông báo lỗi ở trên cần phải biến mất - vì vậy chúng ta sẽ viết một test cho điều này.
- Cuối cùng, khi API trả lại thông báo lỗi, điều này sẽ được hiển thị trong alert dialog - vì vậy chúng ta cũng sẽ thêm một test case.
@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()))
}
Hãy thử chạy chúng:
Và không có gì đáng ngạc nhiên, chúng đã thất bại - đó là vì chúng ta chưa có implementation.
Vì vậy, bây giờ chúng ta cần thêm các implementation cho activity cho đến khi vượt qua các test case. Bởi vì chúng ta đang viết các test case tập trung chỉ test một khái niệm đơn, chúng ta có thể bổ sung các implement một lần và cũng xem các test case đi từng điểm một.
Vì vậy, chúng ta hãy xem xét một trong những testcase không thành công, bắt đầu với test case invalidPasswordErrorDisplayed (). Chúng ta biết một số điều ở đây:
- Để kích hoạt quá trình đăng nhập, người dùng sẽ gõ mật khẩu của họ và sau đó nhấn nút đăng nhập, vì vậy cần phải thực hiện một listener cho nút đăng nhập mà gọi method đăng nhập từ presenter:
private fun setupLoginButtonClickListener() {
button_login.setOnClickListener {
loginPresenter.performSignIn(input_email.text.toString(),
input_password.text.toString()) }
}
- Khi người dùng không nhập mật khẩu vào trường mật khẩu, chúng ta cần phải thực hiện logic để hiển thị thông báo lỗi. Chúng ta đang sử dụng thành phần TextInputLayout vì vậy chúng ta chỉ có thể chỉ định giá trị của thông báo lỗi của nó cho chuỗi lỗi đã xác định trước đó:
override fun showInvalidPasswordMessage() {
layout_input_password.error = getString(R.string.error_message_invalid_password)
}
Và hãy thử chạy lại lần nữa:
Chúng ta sẽ xem test case serverErrorMessageDisplays () tiếp theo. Điều này rất đơn giản, chúng ta biết rằng khi API trả về một phản ứng lỗi (do đó không phải lỗi chung từ thư viện network), ứng dụng sẽ hiển thị thông báo lỗi cho người dùng trong alert dialog. Đối với việc này, chúng ta chỉ cần tạo một thể hiện của dialog, sử dụng thông báo lỗi của server trong text của dialog:
override fun showServerErrorMessage(message: String) {
DialogFactory.createSimpleInfoDialog(this, R.string.error_message_login_title, message,
R.string.error_message_login_ok).show()
}
Tiếp tục chạy test:
Bây giờ chúng ta chỉ còn một test case nữa, đó là test invalidEmailErrorHidesWhenUserTypes (). Một lần nữa đây là một trường hợp đơn giản, nhưng hãy chia nhỏ nó:
- Khi người dùng nhấn nút đăng nhập và không có địa chỉ email, hoặc địa chỉ email không hợp lệ, đã được nhập, chúng ta sẽ hiển thị thông báo lỗi cho người dùng. Chúng ta đã thực hiện điều này, ta đã chỉ loại trừ nó vì sự đơn giản.
- Tuy nhiên, khi người dùng bắt đầu nhập vào field một lần nữa các thông báo lỗi sẽ được gỡ bỏ khỏi View. Đối với điều này, chúng ta cần phải lắng nghe khi nội dung test của trường đầu vào thay đổi:
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)
}
})
}
Cuối cùng, chúng ta chạy test lần nữa:
Nguồn: https://overflow.buffer.com/2017/08/18/exploring-test-driven-development-android-ui-tests/
All Rights Reserved