איך בודקים את האפליקציה Flutter

1. מבוא

Flutter היא ערכת הכלים לבניית ממשק משתמש של Google, שנועדה לבנות אפליקציות יפות ומותאמות לניידים, לאינטרנט ולמחשבים מתוך בסיס קוד יחיד.

ב-Codelab הזה תבנה ותבדוק אפליקציה פשוטה של Flutter. האפליקציה תשתמש בחבילת ה-Provider לניהול המצב.

מה תלמדו

  • איך יוצרים בדיקות של ווידג'טים באמצעות מסגרת הבדיקה של הווידג'טים
  • איך ליצור בדיקת שילוב כדי לבדוק את ממשק המשתמש ואת הביצועים של האפליקציה באמצעות הספרייה integration_test
  • איך בודקים סיווגי נתונים (ספקים) בעזרת בדיקות יחידה (unit testing)

מה תפַתחו

ב-Codelab הזה, תתחילו בפיתוח אפליקציה פשוטה עם רשימת פריטים. אנחנו מספקים לכם את קוד המקור כדי שתוכלו להתחיל את הבדיקה ישירות. האפליקציה תומכת בפעולות הבאות:

  • הפריטים מתווספים למועדפים
  • צפייה ברשימת המועדפים
  • הסרת פריטים מרשימת המועדפים

לאחר השלמת האפליקציה, תכתבו את הבדיקות הבאות:

  • בדיקות יחידה (unit testing) כדי לאמת את פעולות ההוספה וההסרה
  • בדיקות ווידג'טים לדף הבית ולדף המועדפים
  • בדיקות ממשק משתמש וביצועים עבור האפליקציה כולה, באמצעות בדיקות שילוב

GIF של האפליקציה שפועלת ב-Android

מה היית רוצה ללמוד מ-Codelab הזה?

הנושא חדש ורציתי לקבל סקירה כללית טובה. יש לי מושג בנושא הזה, אבל אני רוצה לרענן את הידע שלי. אני רוצה קוד לדוגמה לשימוש בפרויקט שלי. אני רוצה הסבר למשהו ספציפי.

2. הגדרת סביבת הפיתוח של Flutter

כדי להשלים את שיעור ה-Lab הזה אתם צריכים שתי תוכנות: Flutter SDK וכלי עריכה.

אפשר להריץ את Codelab באמצעות כל אחד מהמכשירים הבאים:

  • מכשיר פיזי שמשמש ל-Android או ל-iOS שמחובר למחשב ומוגדר ל'מצב פיתוח'.
  • הסימולטור של iOS (צריך להתקין כלים של Xcode).
  • האמולטור של Android (נדרשת הגדרה ב-Android Studio).
  • דפדפן (Chrome נדרש לניפוי באגים).
  • בתור אפליקציית Windows , Linux או macOS למחשב. צריך לפתח בפלטפורמה שבה אתם מתכננים לפרוס. לכן, כדי לפתח אפליקציה למחשב של Windows, צריך לפתח את האפליקציה ב-Windows כדי לגשת לשרשרת ה-build המתאימה. יש דרישות ספציפיות למערכת ההפעלה שמפורטות בהרחבה בכתובת docs.flutter.dev/desktop.

3. תחילת העבודה

יצירת אפליקציית Flutter חדשה לעדכן את יחסי התלות

ה-Codelab הזה מתמקד בבדיקה של אפליקציה לנייד של 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להוסיף יחסי תלות של pub בשורת הפקודה.

  • provider כדי לנהל את המצב בקלות,
  • integration_test לבדיקה עצמאית של קוד Flutter במכשירים ואמולטורים,
  • flutter_driver לקבלת API מתקדם לבדיקת אפליקציות 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 ולקובצי Dat יש את אותו שם עם הסיומת _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 מתחיל את התהליך על ידי הוראה ל-framework לטעון ולמדוד ווידג'ט מסוים בדיוק כמו באפליקציה.

מסגרת הבדיקה של ווידג'טים מאפשרת לחיפוש ווידג'טים, לדוגמה 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. הווידג'ט של דף הבית צריך ששני הווידג'טים האלה יוצגו מעליו בעץ הווידג'טים, כדי שהוא יוכל לרשת מהם ולקבל גישה לנתונים שהוא מציע. הפונקציה הזו מועברת כפרמטר לפונקציה pumpWidget().

בשלב הבא צריך לבדוק אם ה-framework יכול למצוא 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. הפעלת ה-Hot הזו מפעילה מחדש את האפליקציה ומריצה מחדש את כל הבדיקות.

קובץ הבדיקה המלא: 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, Protractor, 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

כתיבה של בדיקת ביצועים

יוצרים בתיקייה integration_test קובץ בדיקה חדש בשם perf_test.dart עם התוכן הבא:

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.

אחרי שהבדיקה תסתיים בהצלחה, ספריית ה-build שברמה הבסיסית (root) של הפרויקט מכילה שני קבצים:

  1. הסיכום מכיל את scrolling_summary.timeline_summary.json. פותחים את הקובץ באמצעות כלי כלשהו לעריכת טקסט כדי לבדוק את המידע שכלול בו.
  2. scrolling_summary.timeline.json מכיל את כל הנתונים של ציר הזמן.

לפרטים נוספים על בדיקות אינטגרציה:

9. מעולה!

השלמת את ה-Codelab ולמדת דרכים שונות לבדוק אפליקציית Flutter.

מה למדת

  • איך בודקים ספקים בעזרת בדיקות יחידה (unit testing)
  • איך בודקים ווידג'טים באמצעות מסגרת הבדיקה של הווידג'טים
  • איך בודקים את ממשק המשתמש של האפליקציה באמצעות בדיקות שילוב
  • איך בודקים את הביצועים של האפליקציה באמצעות בדיקות שילוב

למידע נוסף על בדיקות ב-Flutter, אפשר להיכנס לאתר