إتقان التطوير من خلال الاختبار في Android باستخدام اختبارات واجهة المستخدم

تحية للجميع. تحسبا لإطلاق مجموعة جديدة من الأساسية و المتقدمة دورات في تطوير الروبوت، ونحن على استعداد لترجمة مواد مثيرة للاهتمام.




خلال العام الماضي من عمل فريق تطوير Android في Buffer ، تحدثنا كثيرًا عن نظافة مشروعنا وتحسين استقراره. أحد العوامل كان تقديم () الاختبارات ، والتي ، كما اكتشفنا بالفعل ، تساعدنا على تجنب الانحدار في التعليمات البرمجية الخاصة بنا وتعطينا المزيد من الثقة في الميزات التي نقدمها. والآن ، عندما نطلق منتجات جديدة في Buffer ، نريد أن نتأكد من تطبيق نفس النهج عندما يتعلق الأمر بها - فقط حتى لا نكون في نفس الوضع كما كان من قبل.

خلفية


عند كتابة اختبارات الوحدة لتطبيقات التخزين المؤقت ، التزمنا دائمًا بممارسة التطوير من خلال الاختبار (التطوير القائم على الاختبار - المشار إليه فيما يلي باسم TDD). هناك العديد من الموارد حول ماهية TDD ، ومزاياه ، ومن أين أتوا ، لكننا لن نتطرق إلى هذا الموضوع ، نظرًا لوجود الكثير من المعلومات على الإنترنت. على مستوى عال ، ألاحظ شخصياً بعض منهم:

  • تقليل وقت التطوير
  • كود أبسط وأنظف وأكثر صيانة
  • كود أكثر موثوقية مع ثقة أكبر في عملنا.
  • تغطية اختبار أعلى (يبدو هذا واضحًا)

ولكن حتى وقت قريب ، اتبعنا فقط مبادئ TDD فقط في شكل اختبارات وحدة لعمليات التنفيذ التي لم تستند إلى واجهة المستخدم ...



أعلم ، أعلم ... لقد اعتدنا دائمًا على كتابة اختبارات واجهة المستخدم بعد اكتمال ما تم تنفيذه - وهذا لا معنى له. لقد قمنا بمراقبة TDD للواجهة الخلفية حتى يفي الرمز الذي نكتبه بالمتطلبات التي تحددها الاختبارات ، ولكن عندما يتعلق الأمر باختبارات واجهة المستخدم ، فإننا نكتب اختبارات تلبي تنفيذ ميزة معينة. كما ترون ، هذا مثير للجدل بعض الشيء ، وبطريقة ما يتعلق الأمر باستخدام سبب TDD في المقام الأول.

لذا ، أود هنا أن أفكر في سبب ذلك وكيف نجرب التغيير. ولكن لماذا ننتبه لهذا في المقام الأول؟

كان من الصعب دائمًا العمل مع الأنشطة الحالية في تطبيقنا بسبب كيفية كتابتها. هذا ليس عذرًا كاملاً ، لكن تبعياته العديدة ومسؤولياته وعلاقاته الوثيقة تجعل من الصعب للغاية اختبارها. بالنسبة للأنشطة الجديدة التي أضفناها ، خارج العادة ، كنت دائمًا ما أكتب اختبارات واجهة المستخدم بعد التنفيذ - إلى جانب العادة ، لم يكن هناك سبب آخر لذلك. ومع ذلك ، عند إنشاء رمز القالب ، جاهزًا للمشاريع الجديدة ، فكرت في التغيير. وسوف نكون سعداء لمعرفة أن هذه العادة قد كسر، ونحن الآن نعمل على أنفسنا، واستكشاف TDD ل 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 () ، نقوم ببساطة بتعيين رابط للتخطيط ، ولدينا أيضًا رابط إلى واجهة العرض الخاصة بنا ، والتي يتم تنفيذها باستخدام النشاط ، ولكن ليس لديهم تطبيقات بعد.

أحد أكثر الأشياء شيوعًا التي نجدها في اختبارات Espresso هي طرق العرض المرجعية والسلاسل حسب معرفات الموارد الموجودة في تطبيقنا. في هذا الصدد ، نحتاج مرة أخرى إلى توفير ملف تخطيط للاستخدام في نشاطنا. هذا يرجع إلى حقيقة ما يلي: أ) نشاطنا يحتاج إلى ملف تخطيط لعرض التخطيط أثناء الاختبارات ، و ب) نحتاج إلى عرض معرف للروابط في اختباراتنا. دعنا نمضي قدمًا ونرسم تخطيطًا بسيطًا جدًا لنشاط تسجيل الدخول:

<?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 أو فئة النشاط ، قم بتعريفها فقط في الملف strings.xml.

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

قد تلاحظ أننا نكتب في هذا الإعداد أقل قدر ممكن ، ولكننا نقدم تفاصيل كافية حول نشاطنا وتخطيطه لكتابة اختبارات له. لا يعمل نشاطنا حاليًا ، ولكنه يفتح ولديه طريقة عرض يمكن الرجوع إليها. الآن لدينا الحد الأدنى من العمل ، دعنا نكمل ونضيف بعض الاختبارات.

مضيفا الاختبارات


لذلك ، لدينا ثلاث حالات يجب علينا تنفيذها ، لذلك سنكتب بعض الاختبارات لها.

  • عندما يقوم مستخدم بإدخال عنوان بريد إلكتروني غير صالح في حقل إدخال البريد الإلكتروني ، نحتاج إلى عرض رسالة خطأ. لذا ، سنقوم بكتابة اختبار يتحقق مما إذا كانت رسالة الخطأ هذه معروضة.
  • عندما يبدأ المستخدم في إدخال البيانات في حقل إدخال البريد الإلكتروني مرة أخرى ، يجب أن تختفي رسالة الخطأ أعلاه - وبالتالي سنقوم بكتابة اختبار لهذا.
  • أخيرًا ، عندما تعرض واجهة برمجة التطبيقات رسالة خطأ ، يجب عرضها في مربع حوار تحذير - لذلك سنضيف أيضًا اختبارًا لذلك.

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

حسنًا ، لقد كتبنا الآن اختبارات - فلنواصلها ونديرها.



وليس من المستغرب أنهم فشلوا - هذا لأننا لم ننفذ بعد ، لذا يجب توقع ذلك. على أي حال ، يجب أن نكون سعداء لرؤية الأحمر للاختبارات الآن!

لذا نحتاج الآن إلى إضافة تطبيقات لنشاطنا حتى تمر الاختبارات. بينما نكتب اختبارات مركزة تختبر مفهومًا واحدًا فقط (أو على الأقل يجب أن يكون!) ، يمكننا إضافة عمليات التنفيذ واحدة تلو الأخرى وكذلك مشاهدة اختباراتنا تتحول إلى اللون الأخضر تلو الآخر.

لذا ، فلنلقِ نظرة على أحد الاختبارات الفاشلة ، فلنبدأ باختبار غير صالح PassPasswordErrorDisplayed () . نحن نعرف بعض الأشياء:

  • , , , , :

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(). هذا أمر بسيط للغاية ، نحن نعلم أنه عندما تقوم واجهة برمجة التطبيقات بإرجاع استجابة خطأ (وليس خطأ عامًا من مكتبة الشبكة الخاصة بنا) ، يجب أن يعرض التطبيق رسالة خطأ للمستخدم في مربع حوار تحذير. للقيام بذلك ، نحتاج فقط إلى إنشاء مثيل من مربع الحوار باستخدام رسالة خطأ الخادم الخاصة بنا في نص الحوار:

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 بدائي للغاية. تخيل أننا نقوم بتطوير شاشة معقدة ، مثل موجز المحتوى ، حيث يمكنك تنفيذ العديد من الإجراءات باستخدام عناصر الخلاصة (على سبيل المثال ، كما هو الحال في تطبيق Buffer لنظام Android) - في هذه الحالات ، سنستخدم العديد من الوظائف المختلفة التي يجب تنفيذها في النشاط المحدد / شظية. هذه هي الحالات التي سيتم فيها الكشف عن TDD في اختبارات واجهة المستخدم أكثر من ذلك ، لأن ما يمكن أن يؤدي إلى حقيقة أننا سنكتب كودًا معقدًا للغاية لهذه الوظائف يمكن اختزاله إلى عمليات التنفيذ التي تلبي الاختبارات المعينة التي كتبناها.

للتلخيص ، سأشارك بعض النقاط المستفادة من تجربتي:

  • , , TDD . , . , Espresso (/, ), . activity, , , . .
  • , , , , , , , . , , , , , ( , !). - . , , , - .
  • , Ui- , , . , , , , , , .
  • , . , — , , , - .
  • يبدو أكثر طبيعية. نظرًا لحقيقة أن TDD مستخدم بالفعل في اختبارات الوحدة ، فإنك تشعر بالخلف قليلاً عند كتابة اختبارات الوحدة ، تليها عمليات التنفيذ ، تليها اختبارات واجهة المستخدم. ستشعر بمزيد من الطبيعي من خلال الانتقال الكامل مع TDD ، وليس في منتصف الطريق.

هل تستخدم TDD بالفعل عند كتابة اختبارات واجهة المستخدم وتقوم بشيء مماثل أو مختلف تمامًا؟ أو هل تريد معرفة المزيد وطرح بعض الأسئلة؟ لا تتردد في التعليق أدناه أو إرسال تغريدة لنا على @bufferdevs

هذا كل شيء. نحن في انتظارك في الدورات:


All Articles