أعتقد أن الكثير من الناس يعرفون بالفعل 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(); } }
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
.