كيفية اختبار تطبيق Flutter

1. مقدمة

Flutter هو مجموعة أدوات واجهة المستخدم من Google لإنشاء تطبيقات رائعة ومُجمَّعة إلى رموز أصلية للأجهزة الجوّالة والويب وأجهزة الكمبيوتر المكتبي، وذلك من خلال قاعدة رموز برمجية واحدة.

في هذا الدرس التطبيقي حول الترميز، ستتمكّن من إنشاء تطبيق بسيط من Flutter واختباره. سيستخدم التطبيق حزمة الموفّر لإدارة الحالة.

ما ستتعرَّف عليه

  • كيفية إنشاء اختبارات التطبيقات المصغّرة باستخدام إطار عمل اختبار التطبيقات المصغّرة
  • كيفية إنشاء اختبار دمج لاختبار واجهة مستخدم التطبيق وأدائه باستخدام مكتبة integration_test
  • كيفية اختبار فئات البيانات (الموفّرين) بمساعدة اختبارات الوحدات

ما الذي ستقوم ببنائه

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

  • إضافة العناصر إلى المفضلة
  • عرض قائمة الأجهزة المفضَّلة
  • إزالة عناصر من قائمة "المفضلات"

بعد اكتمال التطبيق، ستكتب الاختبارات التالية:

  • اختبارات الوحدة للتحقّق من صحة عمليات الإضافة والإزالة
  • اختبارات التطبيقات المصغّرة للصفحة الرئيسية والصفحات المفضّلة
  • اختبارات واجهة المستخدم والأداء للتطبيق بالكامل باستخدام اختبارات الدمج

ملف GIF للتطبيق الذي يعمل على نظام Android

ما الذي تريد تعلّمه من هذا الدرس التطبيقي حول الترميز؟

أنا جديد في هذا الموضوع، وأريد نظرة عامة جيدة. أعرف معلومات عن هذا الموضوع، ولكن أريد تنشيطًا للذاكرة. أبحث عن رمز برمجي لاستخدامه في مشروعي. أريد تفسيرًا لمعلومة محدّدة.

2. إعداد بيئة تطوير Flutter

لإكمال هذا التمرين، تحتاج إلى برنامجَين، وهما Flutter SDK ومحرِّر.

يمكنك تشغيل الدرس التطبيقي حول الترميز باستخدام أي من الأجهزة التالية:

  • جهاز Android أو iOS فعلي متصل بجهاز الكمبيوتر وتم ضبطه على "وضع مطور البرامج".
  • محاكي iOS (يتطلب تثبيت أدوات Xcode).
  • محاكي Android (يتطلب عملية إعداد في "استوديو Android").
  • متصفّح (يجب توفُّر متصفّح Chrome لتصحيح الأخطاء)
  • كتطبيق سطح المكتب الذي يعمل بنظام التشغيل Windows أو Linux أو macOS. يجب إجراء تطوير على النظام الأساسي الذي تخطّط لنشر الإعلان عليه. لذا، إذا كنت ترغب في تطوير تطبيق سطح مكتب Windows، ينبغي لك تطويره على Windows للوصول إلى سلسلة الإصدار المناسبة. هناك متطلبات خاصة بنظام التشغيل تم تناولها بالتفصيل على docs.flutter.dev/desktop.

3- الخطوات الأولى

إنشاء تطبيق Flutter جديد تحديث التبعيات

يركّز هذا الدرس التطبيقي حول الترميز على اختبار تطبيق Flutter للأجهزة الجوّالة. ستُنشئ التطبيق سريعًا ليتم اختباره باستخدام ملفات المصدر التي تنسخها وتلصقها. ويركّز باقي الدرس التطبيقي حول الترميز على تعلُّم أنواع مختلفة من الاختبارات.

a3c16fc17be25f6c.pngأنشِئ تطبيق Flutter بسيط وفقًا للنموذج، إمّا باتّباع التعليمات الواردة في بدء استخدام أول تطبيق Flutter أو في سطر الأوامر على النحو التالي.

$ flutter create --empty testing_app
Creating project testing_app...
Resolving dependencies in `testing_app`... 
Downloading packages... 
Got dependencies in `testing_app`.
Wrote 128 files.

All done!
You can find general documentation for Flutter at: https://docs.flutter.dev/
Detailed API documentation is available at: https://api.flutter.dev/
If you prefer video documentation, consider: https://www.youtube.com/c/flutterdev

In order to run your empty application, type:

  $ cd testing_app
  $ flutter run

Your empty application code is in testing_app/lib/main.dart.

a3c16fc17be25f6c.pngأضِف التبعيات التي تخصّ الحانات إلى سطر الأوامر.

  • provider لإدارة الحالات بسهولة
  • integration_test لإجراء اختبار القيادة الذاتي لرمز Flutter على الأجهزة وأجهزة المحاكاة،
  • flutter_driver للحصول على واجهة برمجة تطبيقات متقدّمة لاختبار تطبيقات Flutter التي تعمل على أجهزة ومحاكيات حقيقية،
  • test لأدوات الاختبار العامة
  • go_router للتعامل مع التنقّل بين التطبيقات.
$ cd testing_app
$ flutter pub add provider go_router dev:test 'dev:flutter_driver:{"sdk":"flutter"}' 'dev:integration_test:{"sdk":"flutter"}'
Resolving dependencies... 
Downloading packages... 
+ _fe_analyzer_shared 67.0.0 (68.0.0 available)
+ analyzer 6.4.1 (6.5.0 available)
+ args 2.5.0
+ convert 3.1.1
+ coverage 1.7.2
+ crypto 3.0.3
+ file 7.0.0
+ flutter_driver 0.0.0 from sdk flutter
+ flutter_web_plugins 0.0.0 from sdk flutter
+ frontend_server_client 4.0.0
+ fuchsia_remote_debug_protocol 0.0.0 from sdk flutter
+ glob 2.1.2
+ go_router 14.0.2
+ http_multi_server 3.2.1
+ http_parser 4.0.2
+ integration_test 0.0.0 from sdk flutter
+ io 1.0.4
+ js 0.7.1
  leak_tracker 10.0.4 (10.0.5 available)
  leak_tracker_flutter_testing 3.0.3 (3.0.5 available)
+ logging 1.2.0
  material_color_utilities 0.8.0 (0.11.1 available)
  meta 1.12.0 (1.14.0 available)
+ mime 1.0.5
+ nested 1.0.0
+ node_preamble 2.0.2
+ package_config 2.1.0
+ platform 3.1.4
+ pool 1.5.1
+ process 5.0.2
+ provider 6.1.2
+ pub_semver 2.1.4
+ shelf 1.4.1
+ shelf_packages_handler 3.0.2
+ shelf_static 1.1.2
+ shelf_web_socket 1.0.4
+ source_map_stack_trace 2.1.1
+ source_maps 0.10.12
+ sync_http 0.3.1
+ test 1.25.2 (1.25.4 available)
  test_api 0.7.0 (0.7.1 available)
+ test_core 0.6.0 (0.6.2 available)
+ typed_data 1.3.2
+ watcher 1.1.0
+ web 0.5.1
+ web_socket_channel 2.4.5
+ webdriver 3.0.3
+ webkit_inspection_protocol 1.2.1
+ yaml 3.1.2
Changed 44 dependencies!
9 packages have newer versions incompatible with dependency constraints.
Try `flutter pub outdated` for more information.

يجب إضافة التبعيات التالية إلى pubspec.yaml:

pubspec.yaml

name: testing_app
description: "A new Flutter project."
publish_to: 'none'
version: 0.1.0

environment:
  sdk: '>=3.4.0-0 <4.0.0'

dependencies:
  flutter:
    sdk: flutter
  go_router: ^14.0.2
  provider: ^6.1.2

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^3.0.0
  test: ^1.25.2
  flutter_driver:
    sdk: flutter
  integration_test:
    sdk: flutter

flutter:
  uses-material-design: true

a3c16fc17be25f6c.pngافتح المشروع في أداة تعديل الرموز التي تختارها وشغِّل التطبيق. وبدلاً من ذلك، يمكنك تشغيله في سطر الأوامر على النحو التالي.

$ flutter run

4. إنشاء التطبيق

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

  • lib/models/favorites.dart - ينشئ فئة النموذج لقائمة المفضلة
  • lib/screens/favorites.dart - لإنشاء تنسيق قائمة الأجهزة المفضَّلة
  • lib/screens/home.dart - ينشئ قائمة بالعناصر
  • lib/main.dart - الملف الرئيسي الذي يبدأ منه التطبيق

أولاً، عليك إنشاء نموذج Favorites في "lib/models/favorites.dart".

a3c16fc17be25f6c.pngأنشِئ دليلاً جديدًا باسم models في الدليل lib، ثم أنشِئ ملفًا جديدًا باسم favorites.dart. في هذا الملف، أضف التعليمة البرمجية التالية:

lib/models/favorites.dart

import 'package:flutter/material.dart';

/// The [Favorites] class holds a list of favorite items saved by the user.
class Favorites extends ChangeNotifier {
  final List<int> _favoriteItems = [];

  List<int> get items => _favoriteItems;

  void add(int itemNo) {
    _favoriteItems.add(itemNo);
    notifyListeners();
  }

  void remove(int itemNo) {
    _favoriteItems.remove(itemNo);
    notifyListeners();
  }
}

إضافة صفحة "التسجيلات المفضّلة" في "lib/screens/favorites.dart"

a3c16fc17be25f6c.pngأنشِئ دليلاً جديدًا باسم screens في الدليل lib، وفي هذا الدليل أنشِئ ملفًا جديدًا باسم favorites.dart. في هذا الملف، أضف التعليمة البرمجية التالية:

lib/screens/favorites.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import '../models/favorites.dart';

class FavoritesPage extends StatelessWidget {
  const FavoritesPage({super.key});

  static String routeName = 'favorites_page';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Favorites'),
      ),
      body: Consumer<Favorites>(
        builder: (context, value, child) => ListView.builder(
          itemCount: value.items.length,
          padding: const EdgeInsets.symmetric(vertical: 16),
          itemBuilder: (context, index) => FavoriteItemTile(value.items[index]),
        ),
      ),
    );
  }
}

class FavoriteItemTile extends StatelessWidget {
  const FavoriteItemTile(this.itemNo, {super.key});

  final int itemNo;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: ListTile(
        leading: CircleAvatar(
          backgroundColor: Colors.primaries[itemNo % Colors.primaries.length],
        ),
        title: Text(
          'Item $itemNo',
          key: Key('favorites_text_$itemNo'),
        ),
        trailing: IconButton(
          key: Key('remove_icon_$itemNo'),
          icon: const Icon(Icons.close),
          onPressed: () {
            Provider.of<Favorites>(context, listen: false).remove(itemNo);
            ScaffoldMessenger.of(context).showSnackBar(
              const SnackBar(
                content: Text('Removed from favorites.'),
                duration: Duration(seconds: 1),
              ),
            );
          },
        ),
      ),
    );
  }
}

إضافة الصفحة الرئيسية في lib/screens/home.dart

a3c16fc17be25f6c.pngفي الدليل lib/screens، أنشِئ ملفًا جديدًا آخر باسم home.dart. في lib/screens/home.dart، أضِف الرمز التالي:

lib/screens/home.dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import '../models/favorites.dart';
import 'favorites.dart';

class HomePage extends StatelessWidget {
  static String routeName = '/';

  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Testing Sample'),
        actions: <Widget>[
          TextButton.icon(
            onPressed: () {
              context.go('/${FavoritesPage.routeName}');
            },
            icon: const Icon(Icons.favorite_border),
            label: const Text('Favorites'),
          ),
        ],
      ),
      body: ListView.builder(
        itemCount: 100,
        cacheExtent: 20.0,
        padding: const EdgeInsets.symmetric(vertical: 16),
        itemBuilder: (context, index) => ItemTile(index),
      ),
    );
  }
}

class ItemTile extends StatelessWidget {
  final int itemNo;

  const ItemTile(this.itemNo, {super.key});

  @override
  Widget build(BuildContext context) {
    var favoritesList = Provider.of<Favorites>(context);

    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: ListTile(
        leading: CircleAvatar(
          backgroundColor: Colors.primaries[itemNo % Colors.primaries.length],
        ),
        title: Text(
          'Item $itemNo',
          key: Key('text_$itemNo'),
        ),
        trailing: IconButton(
          key: Key('icon_$itemNo'),
          icon: favoritesList.items.contains(itemNo)
              ? const Icon(Icons.favorite)
              : const Icon(Icons.favorite_border),
          onPressed: () {
            !favoritesList.items.contains(itemNo)
                ? favoritesList.add(itemNo)
                : favoritesList.remove(itemNo);
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(
                content: Text(favoritesList.items.contains(itemNo)
                    ? 'Added to favorites.'
                    : 'Removed from favorites.'),
                duration: const Duration(seconds: 1),
              ),
            );
          },
        ),
      ),
    );
  }
}

استبدال محتوى lib/main.dart

a3c16fc17be25f6c.pngاستبدِل محتوى lib/main.dart بالرمز التالي:

lib/main.dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'models/favorites.dart';
import 'screens/favorites.dart';
import 'screens/home.dart';

void main() {
  runApp(const TestingApp());
}

final _router = GoRouter(
  routes: [
    GoRoute(
      path: HomePage.routeName,
      builder: (context, state) {
        return const HomePage();
      },
      routes: [
        GoRoute(
          path: FavoritesPage.routeName,
          builder: (context, state) {
            return const FavoritesPage();
          },
        ),
      ],
    ),
  ],
);

class TestingApp extends StatelessWidget {
  const TestingApp({super.key});

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<Favorites>(
      create: (context) => Favorites(),
      child: MaterialApp.router(
        title: 'Testing Sample',
        theme: ThemeData(
          colorScheme: ColorScheme.fromSeed(
            seedColor: Colors.deepPurple,
          ),
          useMaterial3: true,
        ),
        routerConfig: _router,
      ),
    );
  }
}

اكتمل التطبيق الآن، ولكنه لم يتم اختباره.

a3c16fc17be25f6c.pngشغِّل التطبيق. يُفترض أن يظهر على النحو التالي لقطة الشاشة التالية:

b74f843e42a28b0f.png

يعرض التطبيق قائمة بالعناصر. انقر على الرمز على شكل قلب في أي صف لملء رمز القلب وإضافة العنصر إلى قائمة العناصر المفضَّلة. ينقلك زر المفضلة على AppBar إلى شاشة ثانية تحتوي على قائمة المفضلة.

التطبيق جاهز الآن للاختبار. ستبدأ في اختبار التطبيق في الخطوة التالية.

5- وحدة اختبار الموفِّر

ستبدأ باختبار الوحدة لنموذج favorites. ما هو اختبار الوحدة؟ يتحقّق اختبار الوحدة من أنّ كل وحدة من البرامج، سواء كانت وظيفة أو كائنًا أو أداة، تؤدي مهمتها المقصودة بشكل صحيح.

يتم وضع جميع ملفات الاختبار في تطبيق Flutter في دليل test، باستثناء اختبارات الدمج.

إزالة "test/widget_test.dart"

a3c16fc17be25f6c.pngقبل بدء الاختبار، يجب حذف ملف widget_test.dart. ستتمكن من إضافة ملفات الاختبار الخاصة بك.

إنشاء ملف اختباري جديد

أولاً، ستختبر طريقة add() في نموذج Favorites للتحقق من إضافة عنصر جديد إلى القائمة، وأن القائمة تعكس التغيير. حسب الاصطلاح، تحاكي بنية الدليل في الدليل test الاسم نفسه في الدليل lib وملفات Dart مع إلحاق _test.

a3c16fc17be25f6c.pngأنشِئ دليل models في الدليل test. في هذا الدليل الجديد، أنشئ ملف favorites_test.dart يتضمّن المحتوى التالي:

test/models/favorites_test.dart

import 'package:test/test.dart';
import 'package:testing_app/models/favorites.dart';

void main() {
  group('Testing App Provider', () {
    var favorites = Favorites();

    test('A new item should be added', () {
      var number = 35;
      favorites.add(number);
      expect(favorites.items.contains(number), true);
    });    
  });
}

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

تستخدم طريقة test() مَعلمتَين موضعيتَين، وهما description للاختبار وcallback حيث تكتب الاختبار.

a3c16fc17be25f6c.pngاختبار إزالة عنصر من القائمة. أدخِل الاختبار التالي في مجموعة Testing App Provider نفسها:

test/models/favorites_test.dart

test('An item should be removed', () {
  var number = 45;
  favorites.add(number);
  expect(favorites.items.contains(number), true);
  favorites.remove(number);
  expect(favorites.items.contains(number), false);
});

إجراء الفحص

a3c16fc17be25f6c.pngفي سطر الأوامر، انتقِل إلى الدليل الجذري للمشروع وأدخِل الأمر التالي:

$ flutter test test/models/favorites_test.dart 

إذا نجح الأمر، من المفترض أن تظهر رسالة مشابهة لما يلي:

00:06 +2: All tests passed!                                                    

الملف التجريبي الكامل: test/models/favorites_test.dart.

لمزيد من المعلومات عن اختبار الوحدة، اطّلِع على المقالة مقدمة عن اختبار الوحدة.

6- اختبار التطبيقات المصغّرة

في هذه الخطوة، ستضيف رمزًا لاختبار التطبيقات المصغّرة. يقتصر اختبار التطبيقات المصغّرة على Flutter، حيث يمكنك اختبار كل تطبيق مصغّر بطريقة منعزلة. تختبر هذه الخطوة شاشتَي "HomePage" و"FavoritesPage" بشكلٍ فردي.

يستخدم اختبار التطبيقات المصغّرة الدالة testWidget() بدلاً من الدالة test(). مثل الدالة test()، تستخدم الدالة testWidget() معلمتين وهما: description, وcallback، ومع ذلك يأخذ الاستدعاء WidgetTester كوسيطة.

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

يتيح إطار عمل اختبار التطبيقات المصغّرة للباحثين العثور على التطبيقات المصغّرة، مثل text() وbyType() وbyIcon().. ويوفّر أيضًا إطار العمل أدوات مطابقة للتحقّق من النتائج.

ابدأ باختبار تطبيق "HomePage" المصغّر.

إنشاء ملف اختباري جديد

يتحقّق الاختبار الأول مما إذا كان الانتقال إلى HomePage يعمل بشكل سليم.

a3c16fc17be25f6c.pngأنشِئ ملفًا جديدًا في دليل test وأدخِل اسمًا له home_test.dart. في الملف الذي تم إنشاؤه حديثًا، أضِف الرمز التالي:

test/home_test.dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:testing_app/models/favorites.dart';
import 'package:testing_app/screens/home.dart';

Widget createHomeScreen() => ChangeNotifierProvider<Favorites>(
      create: (context) => Favorites(),
      child: const MaterialApp(
        home: HomePage(),
      ),
    );

void main() {
  group('Home Page Widget Tests', () {
    testWidgets('Testing Scrolling', (tester) async {
      await tester.pumpWidget(createHomeScreen());
      expect(find.text('Item 0'), findsOneWidget);
      await tester.fling(
        find.byType(ListView),
        const Offset(0, -200),
        3000,
      );
      await tester.pumpAndSettle();
      expect(find.text('Item 0'), findsNothing);
    });
  });
}

تُستخدَم الدالة createHomeScreen() لإنشاء تطبيق يحمِّل التطبيق المصغّر المطلوب اختباره في MaterialApp، ويكون مرتبطًا بـ ChangeNotifierProvider. ويجب أن تكون أداة HomePage موجودةً أعلاه في شجرة الأدوات حتى يمكن اكتسابها منها والوصول إلى البيانات المعروضة عليها. يتمّ تمرير هذه الدالة كمَعلمة إلى الدالة pumpWidget().

بعد ذلك، اختبِر ما إذا كان إطار العمل يمكنه العثور على ListView معروضة على الشاشة.

a3c16fc17be25f6c.pngأضِف مقتطف الرمز التالي إلى home_test.dart:

test/home_test.dart

group('Home Page Widget Tests', () {

  // BEGINNING OF NEW CONTENT
  testWidgets('Testing if ListView shows up', (tester) async {  
    await tester.pumpWidget(createHomeScreen());
    expect(find.byType(ListView), findsOneWidget);
  });                                                
  // END OF NEW CONTENT

    testWidgets('Testing Scrolling', (tester) async {
      await tester.pumpWidget(createHomeScreen());
      expect(find.text('Item 0'), findsOneWidget);
      await tester.fling(
        find.byType(ListView),
        const Offset(0, -200),
        3000,
      );
      await tester.pumpAndSettle();
      expect(find.text('Item 0'), findsNothing);
    });
});

إجراء الفحص

أولاً، نفِّذ الاختبار بالطريقة نفسها المُتّبعة لتشغيل اختبار الوحدة، وذلك باستخدام الأمر:

$ flutter test test/home_test.dart 

من المفترض أن يتم إجراء الاختبار بسرعة، ومن المفترض أن تظهر رسالة مثل هذه:

00:02 +2: All tests passed!                                                    

يمكنك أيضًا إجراء اختبارات التطبيقات المصغّرة باستخدام جهاز أو محاكي يتيح لك مشاهدة الاختبار أثناء تشغيله. ويتيح لك أيضًا استخدام إعادة التشغيل السريع.

a3c16fc17be25f6c.pngابدأ بتوصيل جهازك أو تشغيل المحاكي. يمكنك أيضًا إجراء الاختبار كتطبيق متوافق مع أجهزة الكمبيوتر المكتبي.

a3c16fc17be25f6c.pngمن سطر الأوامر، انتقِل إلى الدليل الجذري للمشروع وأدخِل الأمر التالي:

$ flutter run test/home_test.dart 

قد تحتاج إلى اختيار الجهاز لإجراء الاختبار عليه. في هذه الحالة، اتّبِع التعليمات واختَر جهازًا:

Multiple devices found:
Linux (desktop) • linux  • linux-x64      • Ubuntu 22.04.1 LTS 5.15.0-58-generic
Chrome (web)    • chrome • web-javascript • Google Chrome 109.0.5414.119
[1]: Linux (linux)
[2]: Chrome (chrome)
Please choose one (To quit, press "q/Q"): 

إذا كانت الأمور على ما يرام، فمن المفترض أن تظهر لك نتيجة مشابهة لما يلي:

Launching test/home_test.dart on Linux in debug mode...
Building Linux application...                                           
flutter: 00:00 +0: Home Page Widget Tests Testing if ListView shows up
Syncing files to device Linux...                                    62ms

Flutter run key commands.
r Hot reload. 🔥🔥🔥
R Hot restart.
h List all available interactive commands.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).

💪 Running with sound null safety 💪

An Observatory debugger and profiler on Linux is available at: http://127.0.0.1:35583/GCpdLBqf2UI=/
flutter: 00:00 +1: Home Page Widget Tests Testing Scrolling
The Flutter DevTools debugger and profiler on Linux is available at:
http://127.0.0.1:9100?uri=http://127.0.0.1:35583/GCpdLBqf2UI=/
flutter: 00:02 +2: All tests passed!

بعد ذلك، ستُجري تغييرات على ملف الاختبار وتضغط على Shift + R لإعادة تشغيل التطبيق وإعادة إجراء جميع الاختبارات. يُرجى عدم إيقاف التطبيق.

a3c16fc17be25f6c.pngأضِف المزيد من الاختبارات إلى المجموعة التي تختبر أدوات الصفحة الرئيسية. انسخ الاختبار التالي إلى ملفك:

test/home_test.dart

testWidgets('Testing IconButtons', (tester) async {
  await tester.pumpWidget(createHomeScreen());
  expect(find.byIcon(Icons.favorite), findsNothing);
  await tester.tap(find.byIcon(Icons.favorite_border).first);
  await tester.pumpAndSettle(const Duration(seconds: 1));
  expect(find.text('Added to favorites.'), findsOneWidget);
  expect(find.byIcon(Icons.favorite), findsWidgets);
  await tester.tap(find.byIcon(Icons.favorite).first);
  await tester.pumpAndSettle(const Duration(seconds: 1));
  expect(find.text('Removed from favorites.'), findsOneWidget);
  expect(find.byIcon(Icons.favorite), findsNothing);
});

يتحقّق هذا الاختبار من أنّ النقر على IconButton يتغيّر من Icons.favorite_border (قلب مفتوح) إلى Icons.favorite (قلب مليء بالقلب) ثم يعود إلى Icons.favorite_border عند النقر عليه مرة أخرى.

a3c16fc17be25f6c.pngأدخل Shift + R. يؤدي هذا الإجراء إلى إعادة تشغيل التطبيق وإعادة إجراء جميع الاختبارات.

الملف التجريبي الكامل: test/home_test.dart.

a3c16fc17be25f6c.pngيمكنك استخدام العملية نفسها لاختبار FavoritesPage باستخدام الرمز التالي. اتّبِع الخطوات نفسها وشغِّلها.

test/favorites_test.dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:testing_app/models/favorites.dart';
import 'package:testing_app/screens/favorites.dart';

late Favorites favoritesList;

Widget createFavoritesScreen() => ChangeNotifierProvider<Favorites>(
      create: (context) {
        favoritesList = Favorites();
        return favoritesList;
      },
      child: const MaterialApp(
        home: FavoritesPage(),
      ),
    );

void addItems() {
  for (var i = 0; i < 10; i += 2) {
    favoritesList.add(i);
  }
}

void main() {
  group('Favorites Page Widget Tests', () {
    testWidgets('Test if ListView shows up', (tester) async {
      await tester.pumpWidget(createFavoritesScreen());
      addItems();
      await tester.pumpAndSettle();
      expect(find.byType(ListView), findsOneWidget);
    });

    testWidgets('Testing Remove Button', (tester) async {
      await tester.pumpWidget(createFavoritesScreen());
      addItems();
      await tester.pumpAndSettle();
      var totalItems = tester.widgetList(find.byIcon(Icons.close)).length;
      await tester.tap(find.byIcon(Icons.close).first);
      await tester.pumpAndSettle();
      expect(tester.widgetList(find.byIcon(Icons.close)).length,
          lessThan(totalItems));
      expect(find.text('Removed from favorites.'), findsOneWidget);
    });
  });
}

يتحقّق هذا الاختبار مما إذا كان العنصر يختفي عند الضغط على زر الإغلاق (الإزالة).

لمزيد من المعلومات عن اختبار التطبيقات المصغّرة، يُرجى زيارة:

7. اختبار واجهة المستخدم للتطبيق من خلال اختبارات الدمج

تُستخدم اختبارات التكامل لاختبار كيفية عمل الأجزاء الفردية من التطبيق معًا ككل. يتم استخدام مكتبة integration_test لإجراء اختبارات الدمج في Flutter. هذا إصدار Flutter من إصدار Selenium WebDriver أو المنقلة أو Espresso أو Earl Gray. تستخدم الحزمة flutter_driver داخليًا لإجراء الاختبار على الجهاز.

إنّ كتابة اختبارات الدمج في Flutter تشبه كتابة اختبارات التطبيقات المصغّرة، باستثناء أنّ اختبارات التكامل يتم إجراؤها على جهاز جوّال أو متصفّح أو تطبيق متوافق مع أجهزة الكمبيوتر المكتبي، يُسمى الجهاز المستهدَف.

كتابة الاختبار

a3c16fc17be25f6c.pngأنشِئ دليلاً باسم integration_test في الدليل الجذري للمشروع، وفي هذا الدليل، أنشِئ ملفًا جديدًا باسم app_test.dart.

integration_test/app_test.dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:testing_app/main.dart';

void main() {
  group('Testing App', () {
    testWidgets('Favorites operations test', (tester) async {
      await tester.pumpWidget(const TestingApp());

      final iconKeys = [
        'icon_0',
        'icon_1',
        'icon_2',
      ];

      for (var icon in iconKeys) {
        await tester.tap(find.byKey(ValueKey(icon)));
        await tester.pumpAndSettle(const Duration(seconds: 1));

        expect(find.text('Added to favorites.'), findsOneWidget);
      }

      await tester.tap(find.text('Favorites'));
      await tester.pumpAndSettle();

      final removeIconKeys = [
        'remove_icon_0',
        'remove_icon_1',
        'remove_icon_2',
      ];

      for (final iconKey in removeIconKeys) {
        await tester.tap(find.byKey(ValueKey(iconKey)));
        await tester.pumpAndSettle(const Duration(seconds: 1));

        expect(find.text('Removed from favorites.'), findsOneWidget);
      }
    });
  });
}

إجراء الفحص

a3c16fc17be25f6c.pngابدأ بتوصيل جهازك أو تشغيل المحاكي. يمكنك أيضًا إجراء الاختبار كتطبيق متوافق مع أجهزة الكمبيوتر المكتبي.

a3c16fc17be25f6c.pngفي سطر الأوامر، انتقِل إلى الدليل الجذري للمشروع وأدخِل الأمر التالي:

$ flutter test integration_test/app_test.dart

إذا نجح كل شيء، فمن المفترض أن تظهر لك نتيجة مشابهة لما يلي:

Multiple devices found:
Linux (desktop) • linux  • linux-x64      • Ubuntu 22.04.1 LTS 5.15.0-58-generic
Chrome (web)    • chrome • web-javascript • Google Chrome 109.0.5414.119
[1]: Linux (linux)
[2]: Chrome (chrome)
Please choose one (To quit, press "q/Q"): 1
00:00 +0: loading /home/miquel/tmp/testing_app/integration_test/app_test.dart                                                B00:08 +0: loading /home/miquel/tmp/testing_app/integration_test/app_test.dart                                                
00:26 +1: All tests passed!

8. اختبار أداء التطبيقات باستخدام Flutter Driver

كتابة اختبار أداء

قم بإنشاء ملف اختباري جديد يسمى perf_test.dart في المجلد integration_test بالمحتوى التالي:

integration_test/perf_test.dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:testing_app/main.dart';

void main() {
  group('Testing App Performance', () {
    final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
    binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive;

    testWidgets('Scrolling test', (tester) async {
      await tester.pumpWidget(const TestingApp());

      final listFinder = find.byType(ListView);

      await binding.traceAction(() async {
        await tester.fling(listFinder, const Offset(0, -500), 10000);
        await tester.pumpAndSettle();

        await tester.fling(listFinder, const Offset(0, 500), 10000);
        await tester.pumpAndSettle();
      }, reportKey: 'scrolling_summary');
    });
  });
}

تتحقّق الدالة ensureInitialized() مما إذا تم إعداد برنامج تشغيل اختبار الدمج، وتعيد إعداده إذا لزم الأمر. إنّ ضبط framePolicy على fullyLive مفيد لاختبار الرموز المتحركة.

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

تسجيل نتائج الأداء

للحصول على النتائج، أنشئ مجلدًا باسم "test_driver" يحتوي على ملف باسم "perf_driver.dart"، ثم أضِف الرمز التالي:

test_driver/perf_driver.dart

import 'package:flutter_driver/flutter_driver.dart' as driver;
import 'package:integration_test/integration_test_driver.dart';

Future<void> main() {
  return integrationDriver(
    responseDataCallback: (data) async {
      if (data != null) {
        final timeline = driver.Timeline.fromJson(
            data['scrolling_summary'] as Map<String, dynamic>);

        final summary = driver.TimelineSummary.summarize(timeline);

        await summary.writeTimelineToFile(
          'scrolling_summary',
          pretty: true,
          includeSummary: true,
        );
      }
    },
  );
}

إجراء الفحص

a3c16fc17be25f6c.pngابدأ بتوصيل جهازك أو تشغيل المحاكي.

a3c16fc17be25f6c.pngفي سطر الأوامر، انتقِل إلى الدليل الجذري للمشروع وأدخِل الأمر التالي:

$ flutter drive \
  --driver=test_driver/perf_driver.dart \
  --target=integration_test/perf_test.dart \
  --profile \
  --no-dds

إذا نجح كل شيء، فمن المفترض أن تظهر لك نتيجة مشابهة لما يلي:

Running "flutter pub get" in testing_app...
Resolving dependencies... 
  archive 3.3.2 (3.3.6 available)
  collection 1.17.0 (1.17.1 available)
  js 0.6.5 (0.6.7 available)
  matcher 0.12.13 (0.12.14 available)
  meta 1.8.0 (1.9.0 available)
  path 1.8.2 (1.8.3 available)
  test 1.22.0 (1.23.0 available)
  test_api 0.4.16 (0.4.18 available)
  test_core 0.4.20 (0.4.23 available)
  vm_service 9.4.0 (11.0.1 available)
  webdriver 3.0.1 (3.0.2 available)
Got dependencies!
Running Gradle task 'assembleProfile'...                         1,379ms
✓  Built build/app/outputs/flutter-apk/app-profile.apk (14.9MB).
Installing build/app/outputs/flutter-apk/app-profile.apk...        222ms
I/flutter ( 6125): 00:04 +1: Testing App Performance (tearDownAll)
I/flutter ( 6125): 00:04 +2: All tests passed!
All tests passed.

بعد اكتمال الاختبار بنجاح، يحتوي دليل الإصدار في جذر المشروع على ملفين:

  1. يحتوي scrolling_summary.timeline_summary.json على الملخّص. افتح الملف باستخدام أي محرر نصوص لمراجعة المعلومات الواردة فيه.
  2. يتضمّن scrolling_summary.timeline.json بيانات المخطط الزمني الكاملة.

لمزيد من التفاصيل حول اختبار الدمج، يُرجى زيارة:

9. تهانينا!

لقد أكملت الدرس التطبيقي حول الترميز وتعلّمت طرقًا مختلفة لاختبار تطبيق Flutter.

ما تعلمته

  • كيفية اختبار مقدّمي الخدمات بمساعدة اختبارات الوحدات
  • كيفية اختبار التطبيقات المصغّرة باستخدام إطار عمل اختبار التطبيقات المصغّرة
  • كيفية اختبار واجهة المستخدم الخاصة بالتطبيق باستخدام اختبارات الدمج
  • كيفية اختبار أداء التطبيق باستخدام اختبارات الدمج

لمزيد من المعلومات حول الاختبار في Flutter، يُرجى الانتقال إلى