اختبارات تكامل الرفرفة - إنها سهلة

أعتقد أن الكثير من الناس يعرفون بالفعل Flutter ، وعلى الأقل من أجل الاهتمام ، بدأوا تطبيقات بسيطة عليه. لقد حان الوقت للتأكد من أن كل شيء يعمل حسب حاجته ، وسوف تساعدنا اختبارات الاندماج في ذلك.


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


final SerializableFinder button = find.byValueKey("button"); 

ثم نفذ بعض الإجراءات معهم:


 driver = await FlutterDriver.connect(); ... await driver.tap(button); 

وتحقق من أن عناصر واجهة المستخدم المطلوبة في الحالة المطلوبة:


 final SerializableFinder text = find.byValueKey("text"); expect(await driver.getText(text), "some text"); 

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


كائنات الشاشة


في Android ( Kakao ) ، يتم حل هذه المشكلة عن طريق تجميع عناصر واجهة المستخدم من كل شاشة في شاشة (كائن صفحة). يمكن تطبيق نهج مشابه هنا ، باستثناء أنه في Flutter ، لتنفيذ إجراءات مع عناصر واجهة المستخدم ، لا تحتاج فقط إلى Finder (للبحث عن عنصر واجهة المستخدم) ، ولكن أيضًا FlutterDriver (لتنفيذ إجراء) ، لذلك تحتاج إلى تخزين ارتباط إلى FlutterDriver في Screen .


لتعريف كل عنصر من عناصر واجهة المستخدم ، نضيف فئة DWidget (D - من كلمة Dart في هذه الحالة). لإنشاء DWidget ستحتاج إلى FlutterDriver ، مع الإجراءات التي سيتم تنفيذها على عنصر واجهة المستخدم هذا ، وكذلك ValueKey ، والذي يتزامن مع ValueKey من التطبيق الذي نريد التفاعل معه:


 class DWidget { final FlutterDriver _driver; final SerializableFinder _finder; DWidget(this._driver, dynamic valueKey) : _finder = find.byValueKey(valueKey); ... 

find.byValueKey(…) عند إنشاء كل DWidget يدويًا DWidget مريح ، لذلك من الأفضل تمرير قيمة ValueKey إلى ValueKey ، DWidget نفسها على SerializableFinder المطلوب. كما أنها ليست مريحة للغاية لتمرير FlutterDriver يدويًا عند إنشاء كل DWidget ، بحيث يمكنك تخزين FlutterDriver في BaseScreen ونقله إلى DWidget ، وإضافة طريقة جديدة لـ BaseScreen لإنشاء BaseScreen :


 abstract class BaseScreen { final FlutterDriver _driver; BaseScreen(this._driver); DWidget dWidget(dynamic key) => DWidget(_driver, key); ... 

وبالتالي ، سيكون إنشاء فئات شاشات والحصول على عناصر واجهة المستخدم فيها أسهل بكثير:


 class MainScreen extends BaseScreen { MainScreen(FlutterDriver driver) : super(driver); DWidget get button => dWidget('button'); DWidget get textField => dWidget('text_field'); ... } 

التخلص من await


شيء آخر غير مناسب للغاية عند كتابة الاختبارات باستخدام FlutterDriver هو الحاجة إلى إضافة await قبل كل إجراء:


 await driver.tap(button); await driver.scrollUntilVisible(list, checkBox); await driver.tap(checkBox); await driver.tap(text); await driver.enterText("some text"); 

نسيان await أمر سهل ، وبدون ذلك ، لن تعمل الاختبارات بشكل صحيح ، لأن أساليب driver تُرجع Future<void> وعندما يتم استدعاؤها دون await يتم تنفيذها حتى يتم await أولاً داخل الطريقة ، ويتم "تأجيل باقي الطريقة إلى وقت لاحق".


يمكنك TestAction ذلك عن طريق إنشاء TestAction شأنه "الالتفاف" على Future حتى نتمكن من الانتظار حتى يكتمل إجراء واحد قبل الانتقال إلى التالي:


 typedef TestAction = Future<void> Function(); 

(في الأساس ، TestAction هي أي وظيفة (أو lambda) تُرجع Future<void> )


يمكنك الآن تشغيل تسلسل TestAction بسهولة دون انتظار لا لزوم له:


 Future<void> runTestActions(Iterable<TestAction> actions) async { for (final action in actions) { await action(); } } 

باستخدام TestAction في DWidget


DWidget استخدام DWidget للتفاعل مع عناصر واجهة المستخدم ، وسيكون مناسبًا للغاية إذا كانت هذه الإجراءات هي TestAction بحيث يمكن استخدامها في طريقة runTestAction . للقيام بذلك ، سيكون لفئة DWidget طرق للعمل:


 class DWidget { final FlutterDriver _driver; final SerializableFinder _finder; ... TestAction tap({Duration timeout}) => () => _driver.tap(_finder, timeout: timeout); TestAction setText(String text, {Duration timeout}) => () async { await _driver.tap(_finder, timeout: timeout); await _driver.enterText(text ?? "", timeout: timeout); }; ... } 

الآن يمكنك كتابة الاختبارات على النحو التالي:


 class MainScreen extends BaseScreen { MainScreen(FlutterDriver driver) : super(driver); DWidget get field_1 => dWidget('field_1'); DWidget get field_2 => dWidget('field_2'); DWidget field2Variant(int i) => dWidget('variant_$i'); DWidget get result => dWidget('result'); } … final mainScreen = MainScreen(driver); await runTestActions([ mainScreen.result.hasText("summa = 0"), mainScreen.field_1.setText("3"), mainScreen.field_2.tap(), mainScreen.field2Variant(2).tap(), mainScreen.result.hasText("summa = 5"), ]); 

إذا كنت بحاجة إلى تنفيذ بعض الإجراءات في runTestActions غير المرتبطة بـ DWidget ، فأنت بحاجة فقط إلى إنشاء lambda تقوم بإرجاع Future<void> :


 await runTestActions([ mainScreen.result.hasText("summa = 0"), () => driver.requestData("some_message"), () async => print("some_text"), mainScreen.field_1.setText("3"), ]); 

FlutterDriverHelper


يحتوي FlutterDriver على عدة طرق للتفاعل مع عناصر واجهة المستخدم (الضغط ، استلام النص DWidget ، التمرير ، إلخ.) ولهذه الطرق DWidget لدى DWidget أساليب مماثلة تقوم بإرجاع TestAction .


للراحة ، يتم نشر جميع التعليمات البرمجية الموضحة في هذه المقالة كمكتبة FlutterDriverHelper على pub.dev .


بالنسبة لقوائم التمرير التي يتم فيها إنشاء العناصر ديناميكيًا (على سبيل المثال ، ListView.builder ) ، يحتوي scrollUntilVisible طريقة scrollUntilVisible :


 Future<void> scrollUntilVisible( SerializableFinder scrollable, SerializableFinder item, { double alignment = 0.0, double dxScroll = 0.0, double dyScroll = 0.0, Duration timeout, }) async { ... } 

تقوم هذه الطريقة بالتمرير item واجهة المستخدم القابل للتمرير في الاتجاه المحدد حتى تظهر عنصر واجهة المستخدم على الشاشة (أو حتى timeout ). من أجل عدم scrollable على كل التمرير ، تمت إضافة فئة DScrollItem ، التي ترث DWidget وتمثل عنصر قائمة. يحتوي على رابط scrollable ، لذلك عند التمرير يبقى فقط لتحديد dyScroll أو dxScroll :


 class SecondScreen extends BaseScreen { SecondScreen(FlutterDriver driver) : super(driver); DWidget get list => dWidget("list"); DScrollItem item(int index) => dScrollItem('item_$index', list); } ... final secondScreen = SecondScreen(driver); await runTestActions([ secondScreen.item(42).scrollUntilVisible(dyScroll: -300), ... ]); 

أثناء الاختبارات ، يمكنك التقاط لقطات شاشة للتطبيق ، FlutterDriverHelper على Screenshoter بحفظ لقطات الشاشة للمجلد المطلوب في الوقت الحالي ويمكنه العمل مع TestAction .


مشاكل أخرى وحلولها


  • لم أتمكن من العثور على طريقة قياسية للنقر على الأزرار في مربعات حوار الوقت / التاريخ - يجب أن استخدم TestHooks . يمكن أن يكون TestHooks مفيدًا أيضًا لتغيير الوقت / التاريخ الحالي أثناء الاختبار.
  • في القائمة المنسدلة لـ DropdownButtonFormField تحتاج إلى تحديد key ليس لـ DropdownMenuItem ، ولكن child DropdownMenuItem هذا ، وإلا فلن يتمكن Flutter Driver من العثور عليه. بالإضافة إلى ذلك ، لا يعمل التمرير في القائمة المنسدلة بعد ( العدد على github.com ).
  • طريقة FlutterDriver.getCenter تُرجع Future<DriverOffset> ، لكن DriverOffset ليس جزءًا من واجهة برمجة التطبيقات العامة ( Issue on github.com )
  • هناك عدد قليل من الأشياء أكثر إشكالية وغير واضحة موجودة بالفعل. يمكنك أن تقرأ عنها في مقال رائع . كان من المفيد بشكل خاص القدرة على إجراء الاختبارات على سطح المكتب وإعادة ضبط حالة التطبيق قبل بدء كل اختبار.
  • يمكنك إجراء اختبارات باستخدام Github Actions. مزيد من التفاصيل هنا .

TODO


تتضمن FlutterDriverHelper المستقبل لـ FlutterDriverHelper :


  • التمرير التلقائي إلى عنصر القائمة المرغوب إذا كان وقت الوصول إليه غير مرئي على الشاشة (كما هو الحال في مكتبة Kaspresso لنظام Android). إذا كان ذلك ممكنا ، حتى في كلا الاتجاهين.
  • اعتراضية للإجراءات التي يتم تنفيذها مع Dwidget أو DscrollItem .

التعليقات وردود الفعل البناءة هي موضع ترحيب.


تحديث (01/15/2020) : في الإصدار 1.1.0 أصبح TestAction فئة ، مع حقل String name . وبفضل هذا ، تم إضافة تسجيل جميع الإجراءات التي تم تنفيذها في طريقة runTestActions .

Source: https://habr.com/ru/post/ar483468/


All Articles