Jak przetestować aplikację Flutter

1. Wprowadzenie

Flutter to opracowany przez Google zestaw narzędzi interfejsu do tworzenia pięknych, natywnie skompilowanych aplikacji na urządzenia mobilne, komputery i komputery przy użyciu jednej bazy kodu.

W ramach tego ćwiczenia w programie utworzysz i przetestujesz prostą aplikację Flutter. Aplikacja będzie używać pakietu Provider do zarządzania stanem.

Czego się nauczysz

  • Jak tworzyć testy widżetów za pomocą platformy do testowania widżetów
  • Jak utworzyć test integracji, aby przetestować interfejs i działanie aplikacji za pomocą biblioteki integration_test
  • Jak testować klasy danych (dostawcy) za pomocą testów jednostkowych

Co utworzysz

W tym ćwiczeniu w Codelabs zaczniesz od utworzenia prostej aplikacji z listą elementów. Udostępniamy Ci kod źródłowy, by ułatwić Ci rozpoczęcie testowania. Aplikacja obsługuje te operacje:

  • Dodaję elementy do ulubionych
  • Wyświetlanie listy ulubionych
  • Usuwanie elementów z listy ulubionych

Po zakończeniu pracy aplikacji utworzysz te testy:

  • Testy jednostkowe do weryfikacji operacji dodawania i usuwania
  • Testy widżetów na stronę główną i stronę ulubionych
  • testy interfejsu i wydajności całej aplikacji za pomocą testów integracji.

GIF aplikacji działającej na Androidzie

Czego chcesz się dowiedzieć z tego ćwiczenia z programowania?

Jestem w tym nowym temacie i chcę uzyskać ogólne informacje na ten temat. Wiem coś na ten temat, ale chcę odświeżyć informacje. Szukam przykładowego kodu do użycia w moim projekcie Potrzebuję wyjaśnienia czegoś konkretnego.

2. Konfigurowanie środowiska programistycznego Flutter

Aby ukończyć ten moduł, potrzebujesz 2 oprogramowania: pakietu SDK Flutter i edytora.

Ćwiczenie z programowania możesz uruchomić na dowolnym z tych urządzeń:

  • Fizyczne urządzenie z Androidem lub iOS podłączone do komputera i ustawione w trybie programisty.
  • Symulator iOS (wymaga zainstalowania narzędzi Xcode).
  • Emulator Androida (wymaga skonfigurowania Android Studio).
  • Przeglądarka (do debugowania wymagany jest Chrome).
  • Aplikacja komputerowa w systemie Windows, Linux lub macOS Programowanie należy tworzyć na platformie, na której zamierzasz wdrożyć usługę. Jeśli więc chcesz opracować aplikację komputerową dla systemu Windows, musisz to zrobić w tym systemie, aby uzyskać dostęp do odpowiedniego łańcucha kompilacji. Istnieją wymagania związane z konkretnymi systemami operacyjnymi, które zostały szczegółowo omówione na stronie docs.flutter.dev/desktop.

3. Pierwsze kroki

Utwórz nową aplikację Flutter i zaktualizuj zależności

Skupia się on na testowaniu aplikacji mobilnej Flutter. Szybko utworzysz aplikację do przetestowania, korzystając z plików źródłowych, które skopiujesz i wkleisz. Pozostała część ćwiczeń w Codelabs będzie poświęcona poznawaniu różnych rodzajów testów.

a3c16fc17be25f6c.pngUtwórz prostą aplikację Flutter na podstawie szablonu, postępując zgodnie z instrukcjami w artykule Pierwsze kroki z pierwszą aplikacją Flutter lub korzystając z wiersza poleceń w podany niżej sposób.

$ 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.pngDodaj zależności wydawcy w wierszu poleceń.

  • provider do łatwego zarządzania stanem,
  • integration_test – do samodzielnego testowania kodu Flutter na urządzeniach i emulatorach;
  • flutter_driver – zaawansowany interfejs API do testowania aplikacji Flutter, które działają na prawdziwych urządzeniach i emulatorach,
  • test z ogólnymi narzędziami testowymi,
  • go_router do obsługi nawigacji po aplikacji.
$ 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.

Do pliku pubspec.yaml powinny zostać dodane te zależności:

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.pngOtwórz projekt w wybranym edytorze kodu i uruchom aplikację. Możesz też uruchomić je w wierszu poleceń w następujący sposób:

$ flutter run

4. Utwórz aplikację

Następnie możesz utworzyć aplikację, aby ją przetestować. Aplikacja zawiera te pliki:

  • lib/models/favorites.dart – tworzy klasę modelu dla listy ulubionych
  • lib/screens/favorites.dart – tworzy układ listy ulubionych
  • lib/screens/home.dart – tworzy listę elementów
  • lib/main.dart – główny plik, na którym uruchamia się aplikacja;

Najpierw utwórz model Favorites w obszarze lib/models/favorites.dart.

a3c16fc17be25f6c.pngUtwórz w katalogu lib nowy katalog o nazwie models, a następnie utwórz nowy plik o nazwie favorites.dart. Dodaj do niego ten kod:

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

Dodaj stronę Ulubione w aplikacji lib/screens/favorites.dart.

a3c16fc17be25f6c.pngW katalogu lib utwórz nowy katalog o nazwie screens, a następnie utwórz w nim nowy plik o nazwie favorites.dart. Dodaj do niego ten kod:

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),
              ),
            );
          },
        ),
      ),
    );
  }
}

Dodaj stronę główną w aplikacji lib/screens/home.dart

a3c16fc17be25f6c.pngW katalogu lib/screens utwórz kolejny plik o nazwie home.dart. W lib/screens/home.dart dodaj ten kod:

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),
              ),
            );
          },
        ),
      ),
    );
  }
}

Zastąp zawartość pola lib/main.dart

a3c16fc17be25f6c.pngZastąp zawartość pola lib/main.dart tym kodem:

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,
      ),
    );
  }
}

Aplikacja jest gotowa, ale nie została przetestowana.

a3c16fc17be25f6c.pngUruchom aplikację. Powinien wyglądać podobnie do tego:

b74f843e42a28b0f.png

Aplikacja wyświetla listę elementów. Kliknij ikonę w kształcie serca w dowolnym wierszu, aby wypełnić serce i dodać przedmiot do listy ulubionych. Kliknięcie przycisku Ulubione na urządzeniu AppBar powoduje przejście do drugiego ekranu z listą ulubionych.

Aplikacja jest teraz gotowa do testowania. W następnym kroku zaczniesz testować aplikację.

5. Testowanie jednostkowe dostawcy

Zaczniesz od testowania jednostkowego modelu favorites. Co to jest test jednostkowy? Test jednostkowy polega na tym, że każda jednostka oprogramowania – czy to funkcja, obiekt czy widżet, poprawnie wykonuje swoje zadanie.

Wszystkie pliki testowe w aplikacji Flutter (oprócz testów integracji) znajdują się w katalogu test.

Usuń: test/widget_test.dart

a3c16fc17be25f6c.pngZanim zaczniesz testowanie, usuń plik widget_test.dart. Dodasz własne pliki testowe.

Utwórz nowy plik testowy

Najpierw przetestujesz metodę add() w modelu Favorites, aby sprawdzić, czy do listy został dodany nowy produkt i czy odzwierciedla ona tę zmianę. Zgodnie z konwencją struktura katalogów w katalogu test imituje tę samą nazwę z dołączonym _test w katalogu lib i plikach Dart.

a3c16fc17be25f6c.pngUtwórz katalog models w katalogu test. W nowym katalogu utwórz plik favorites_test.dart o następującej zawartości:

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);
    });    
  });
}

Platforma testowa Flutter umożliwia powiązanie podobnych testów powiązanych ze sobą w grupie. W jednym pliku testowym może znajdować się wiele grup, które służą do testowania różnych części odpowiedniego pliku w katalogu lib.

Metoda test() przyjmuje 2 parametry pozycjonujące: description testu i callback, w którym faktycznie wpisujesz test.

a3c16fc17be25f6c.pngPrzetestuj usuwanie elementu z listy. Wstaw ten test w tej samej grupie 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);
});

Testowanie

a3c16fc17be25f6c.pngW wierszu poleceń przejdź do katalogu głównego projektu i wpisz to polecenie:

$ flutter test test/models/favorites_test.dart 

Jeśli wszystko będzie działać prawidłowo, powinien wyświetlić się komunikat podobny do tego:

00:06 +2: All tests passed!                                                    

Pełny plik testowy: test/models/favorites_test.dart.

Więcej informacji o testowaniu jednostkowym znajdziesz we wprowadzeniu do testowania jednostkowego.

6. Testowanie widżetów

W tym kroku dodasz kod testowania widżetów. Testowanie widżetów jest dostępne tylko w ramach Flutter, gdzie możesz testować każdy widżet w osobny sposób. W tym kroku testujesz poszczególne ekrany HomePage i FavoritesPage.

Testowanie widżetu używa funkcji testWidget() zamiast funkcji test(). Podobnie jak funkcja test(), funkcja testWidget() przyjmuje 2 parametry: description, i callback, jednak wywołanie zwrotne przyjmuje wartość WidgetTester jako argument.

Testy widżetów korzystają z klasy TestFlutterWidgetsBinding, która udostępnia do widżetów te same zasoby, które miałyby w uruchomionej aplikacji, np. informacje o rozmiarze ekranu i możliwości planowania animacji, ale bez uruchamiania aplikacji. Zamiast tego do utworzenia instancji widżetu używane jest środowisko wirtualne, a następnie przeprowadzanie testów wyników. W tym przypadku pumpWidget rozpoczyna ten proces, prosząc platformę o podłączenie i pomiar określonego widżetu w taki sam sposób jak w aplikacji.

Platforma do testowania widżetów zapewnia narzędzia do znajdowania widżetów, np. text(), byType() i byIcon().. Udostępnia też dopasowania, które służą do weryfikowania wyników.

Zacznij od przetestowania widżetu HomePage.

Utwórz nowy plik testowy

Pierwszy test sprawdza, czy przewijanie elementu HomePage działa prawidłowo.

a3c16fc17be25f6c.pngUtwórz nowy plik w katalogu test i nadaj mu nazwę home_test.dart. W nowo utworzonym pliku dodaj ten kod:

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);
    });
  });
}

Funkcja createHomeScreen() służy do tworzenia aplikacji, która wczytuje widżet do przetestowania w MaterialApp, opakowany w zasadę ChangeNotifierProvider. Widżet strony głównej wymaga, aby oba te widżety znajdowały się nad nim w drzewie widżetów, aby mógł je dziedziczyć i uzyskiwać dostęp do oferowanych danych. Ta funkcja jest przekazywana jako parametr do funkcji pumpWidget().

Następnie sprawdź, czy platforma może znaleźć ListView renderowany na ekranie.

a3c16fc17be25f6c.pngDodaj do home_test.dart ten fragment kodu:

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);
    });
});

Testowanie

Najpierw uruchom test w taki sam sposób jak test jednostkowy, używając polecenia:

$ flutter test test/home_test.dart 

Test powinien rozpocząć się szybko i powinno wyświetlić się taki komunikat:

00:02 +2: All tests passed!                                                    

Testy widżetów możesz też przeprowadzić na urządzeniu lub w emulatorze, aby obserwować przebieg testu. Umożliwia też ponowne uruchomienie urządzenia z pamięci.

a3c16fc17be25f6c.pngPodłącz urządzenie lub uruchom emulator. Możesz też uruchomić test jako aplikację komputerową.

a3c16fc17be25f6c.pngZa pomocą wiersza poleceń przejdź do katalogu głównego projektu i wpisz to polecenie:

$ flutter run test/home_test.dart 

Konieczne może być wybranie urządzenia, na którym chcesz przeprowadzić test. W takim przypadku postępuj zgodnie z instrukcjami i wybierz urządzenie:

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"): 

Jeśli wszystko będzie działać, dane wyjściowe powinny wyglądać podobnie do tych:

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!

Następnie wprowadzasz zmiany w pliku testowym i naciskasz Shift + R, aby ponownie uruchomić aplikację na gorąco i ponownie przeprowadzić wszystkie testy. Nie zatrzymuj aplikacji.

a3c16fc17be25f6c.pngDodaj więcej testów do grupy, która będzie testować widżety strony głównej. Skopiuj do pliku ten test:

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);
});

Ten test sprawdza, czy kliknięcie przycisku IconButton zmienia się z Icons.favorite_border (otwarte serce) na Icons.favorite (wypełnione serce), a następnie z powrotem na Icons.favorite_border po ponownym dotknięciu.

a3c16fc17be25f6c.pngWpisz Shift + R. Powoduje to ponowne uruchomienie aplikacji i przeprowadza wszystkie testy od nowa.

Pełny plik testowy: test/home_test.dart.

a3c16fc17be25f6c.pngUżyj tego samego procesu, aby przetestować FavoritesPage z poniższym kodem. Wykonaj te same czynności i uruchom go.

test/ulubione_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);
    });
  });
}

Ten test sprawdza, czy element znika po naciśnięciu przycisku zamykania (usuń).

Więcej informacji o testowaniu widżetów znajdziesz na stronie:

7. Testowanie interfejsu aplikacji za pomocą testów integracji

Testy integracji służą do sprawdzania, jak poszczególne elementy aplikacji współpracują ze sobą. Biblioteka integration_test służy do testowania integracji w technologii Flutter. To jest wersja Selenium WebDriver, Protractor, Espresso lub Earl Gray. Pakiet używa wewnętrznie flutter_driver do przeprowadzania testu na urządzeniu.

Pisanie testów integracji w Flutter przypomina pisanie testów widżetów z tą różnicą, że testy integracji są przeprowadzane na urządzeniach mobilnych, w przeglądarkach lub aplikacjach komputerowych, tzw. urządzeniach docelowych.

Pisanie testu

a3c16fc17be25f6c.pngW katalogu głównym projektu utwórz katalog o nazwie integration_test, a w nim utwórz nowy plik o nazwie 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);
      }
    });
  });
}

Testowanie

a3c16fc17be25f6c.pngPodłącz urządzenie lub uruchom emulator. Możesz też uruchomić test jako aplikację komputerową.

a3c16fc17be25f6c.pngW wierszu poleceń przejdź do katalogu głównego projektu i wpisz to polecenie:

$ flutter test integration_test/app_test.dart

Jeśli wszystko działa, powinny wyświetlić się dane wyjściowe podobne do tych:

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. Testowanie wydajności aplikacji za pomocą wtyczki Flutter Driver

Pisanie testu wydajności

W folderze integration_test utwórz nowy plik testowy o nazwie perf_test.dart z następującą zawartością:

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');
    });
  });
}

Funkcja ensureInitialized() sprawdza, czy sterownik testu integracji został zainicjowany, i w razie potrzeby ponownie go inicjuje. Ustawienie framePolicy na fullyLive jest dobrym rozwiązaniem do testowania animowanego kodu.

W ramach tego testu lista elementów jest przewijana bardzo szybko, a potem przewija się do końca. Funkcja traceAction() rejestruje działania i generuje podsumowanie osi czasu.

Rejestrowanie wyników skuteczności

Aby zapisać wyniki, utwórz folder o nazwie test_driver z plikiem o nazwie perf_driver.dart i dodaj ten kod:

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,
        );
      }
    },
  );
}

Testowanie

a3c16fc17be25f6c.pngPodłącz urządzenie lub uruchom emulator.

a3c16fc17be25f6c.pngW wierszu poleceń przejdź do katalogu głównego projektu i wpisz to polecenie:

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

Jeśli wszystko działa, powinny wyświetlić się dane wyjściowe podobne do tych:

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.

Gdy test się zakończy, katalog kompilacji w katalogu głównym projektu będzie zawierał 2 pliki:

  1. scrolling_summary.timeline_summary.json zawiera podsumowanie. Otwórz plik w dowolnym edytorze tekstu, aby sprawdzić zawarte w nim informacje.
  2. Pole scrolling_summary.timeline.json zawiera pełne dane osi czasu.

Więcej informacji o testowaniu integracji znajdziesz na stronie:

9. Gratulacje!

Udało Ci się ukończyć ćwiczenia i poznać różne sposoby testowania aplikacji Flutter.

Czego się nauczyłeś?

  • Jak sprawdzić dostawców za pomocą testów jednostkowych
  • Jak testować widżety za pomocą platformy do testowania widżetów
  • Jak przetestować interfejs aplikacji za pomocą testów integracji
  • Jak sprawdzić wydajność aplikacji za pomocą testów integracji

Więcej informacji o testowaniu w Flutter znajdziesz na stronie