如何测试 Flutter 应用

1. 简介

Flutter 是 Google 的界面工具包,用于通过单一代码库针对移动设备、Web 和桌面设备构建经过原生编译的精美应用。

在此 Codelab 中,您将构建和测试一个简单的 Flutter 应用。该应用将使用 Provider 软件包来管理状态。

学习内容

  • 如何使用 widget 测试框架创建 widget 测试
  • 如何使用 integration_test 库创建用于测试应用界面和性能的集成测试
  • 如何在单元测试的帮助下测试数据类(提供程序)

构建内容

在此 Codelab 中,您首先要构建一个包含内容列表的简单应用。我们为您提供了源代码,以便您直接进行测试。该应用支持以下操作:

  • 将内容添加到收藏夹
  • 查看收藏夹列表
  • 从收藏夹列表中移除内容

该应用完成后,您将编写以下测试:

  • 用于验证添加和移除操作的单元测试
  • 针对首页和收藏夹页面的 widget 测试
  • 用于使用集成测试测试整个应用的界面和性能测试

在 Android 上运行的应用的 GIF

您想通过此 Codelab 学习哪些内容?

我不熟悉这个主题,想好好了解一下。 我对这个主题有所了解,但想复习并深入了解一下。 我在寻找示例代码以用到我的项目中。 我在寻找有关特定内容的说明。

2. 设置您的 Flutter 开发环境

您需要使用两款软件才能完成此 Codelab:Flutter SDK一款编辑器

您可使用以下任一设备学习此 Codelab:

  • 一台连接到计算机并设置为开发者模式的实体 AndroidiOS 设备。
  • iOS 模拟器(需要安装 Xcode 工具)。
  • Android 模拟器(需要在 Android Studio 中设置)。
  • 浏览器(需要使用 Chrome,以便进行调试)。
  • 对于 WindowsLinuxmacOS 桌面应用,您必须在打算部署到的平台上进行开发。因此,如果您要开发 Windows 桌面应用,则必须在 Windows 上进行开发,才能使用相应的构建链。如需详细了解针对各种操作系统的具体要求,请访问 docs.flutter.dev/desktop

3. 使用入门

创建新的 Flutter 应用并更新依赖项

此 Codelab 将重点介绍如何测试 Flutter 移动应用。您将通过您复制和粘贴的源代码文件快速创建要测试的应用。然后,此 Codelab 的其余部分将重点介绍不同类型的测试。

a3c16fc17be25f6c.png 按照您的第一个 Flutter 应用入门指南中的说明创建一个简单的模板化 Flutter 应用,或者在命令行中按如下所示进行创建。

$ flutter create testing_app

a3c16fc17be25f6c.png 在命令行中添加 pub 依赖项。要轻松进行状态管理,请添加 provider

$ cd testing_app
$ flutter pub add provider
Resolving dependencies...
  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)
+ nested 1.0.0
  path 1.8.2 (1.8.3 available)
+ provider 6.0.5
  test_api 0.4.16 (0.4.18 available)
Changed 2 dependencies!

要在设备和模拟器上对 Flutter 代码进行自我驱动的测试,请添加 integration_test

$ flutter pub add --dev --sdk=flutter integration_test
Resolving dependencies...
+ archive 3.3.2 (3.3.6 available)
  collection 1.17.0 (1.17.1 available)
+ crypto 3.0.2
+ file 6.1.4
+ flutter_driver 0.0.0 from sdk flutter
+ fuchsia_remote_debug_protocol 0.0.0 from sdk flutter
+ integration_test 0.0.0 from sdk flutter
  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)
+ platform 3.1.0
+ process 4.2.4
+ sync_http 0.3.1
  test_api 0.4.16 (0.4.18 available)
+ typed_data 1.3.1
+ vm_service 9.4.0 (11.0.1 available)
+ webdriver 3.0.1 (3.0.2 available)
Changed 12 dependencies!

要使用高级 API 测试在真实设备和模拟器上运行的 Flutter 应用,请添加 flutter_driver

$ flutter pub add --dev --sdk=flutter flutter_driver
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_api 0.4.16 (0.4.18 available)
  vm_service 9.4.0 (11.0.1 available)
  webdriver 3.0.1 (3.0.2 available)
Got dependencies!

要使用常规测试工具,请添加 test

$ flutter pub add --dev test
Resolving dependencies...
+ _fe_analyzer_shared 52.0.0
+ analyzer 5.4.0
  archive 3.3.2 (3.3.6 available)
+ args 2.3.2
  collection 1.17.0 (1.17.1 available)
+ convert 3.1.1
+ coverage 1.6.3
+ frontend_server_client 3.2.0
+ glob 2.1.1
+ http_multi_server 3.2.1
+ http_parser 4.0.2
+ io 1.0.4
  js 0.6.5 (0.6.7 available)
+ logging 1.1.1
  matcher 0.12.13 (0.12.14 available)
  meta 1.8.0 (1.9.0 available)
+ mime 1.0.4
+ node_preamble 2.0.1
+ package_config 2.1.0
  path 1.8.2 (1.8.3 available)
+ pool 1.5.1
+ pub_semver 2.1.3
+ shelf 1.4.0
+ shelf_packages_handler 3.0.1
+ shelf_static 1.1.1
+ shelf_web_socket 1.0.3
+ source_map_stack_trace 2.1.1
+ source_maps 0.10.11
+ 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)
+ watcher 1.0.2
+ web_socket_channel 2.3.0
  webdriver 3.0.1 (3.0.2 available)
+ webkit_inspection_protocol 1.2.0
+ yaml 3.1.1
Changed 28 dependencies!

要处理应用导航,请添加 go_router

$ flutter pub add go_router
Resolving dependencies...
  archive 3.3.2 (3.3.6 available)
  collection 1.17.0 (1.17.1 available)
+ flutter_web_plugins 0.0.0 from sdk flutter
+ go_router 6.0.4
  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)
Changed 2 dependencies!

以下依赖项应已添加到您的 pubspec.yaml

dependencies 下:

dependencies:
  provider: ^6.0.5
  go_router: ^6.0.4

dev_dependencies 下:

dev_dependencies:
  integration_test:
    sdk: flutter
  flutter_driver:
    sdk: flutter
  test: ^1.22.0

a3c16fc17be25f6c.png 在您所选的代码编辑器中打开项目,然后运行应用。或者,在命令行中按如下所示运行应用。

$ flutter run

4. 构建应用

接下来,您需要构建应用,以便进行测试。该应用包含以下文件:

  • lib/models/favorites.dart - 用于为收藏夹列表创建模型类
  • lib/screens/favorites.dart - 用于为收藏夹列表创建布局
  • lib/screens/home.dart - 用于创建内容列表
  • lib/main.dart - 应用启动所在的主文件

首先,在 lib/models/favorites.dart 中创建 Favorites 模型

a3c16fc17be25f6c.pnglib 目录中创建名为 models 的新目录,然后创建名为 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 中添加“Favorites”页面

a3c16fc17be25f6c.pnglib 目录中创建名为 screens 的新目录,然后在该目录中创建名为 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.pnglib/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.pnglib/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(
          primarySwatch: Colors.blue,
          useMaterial3: true,
        ),
        routerConfig: _router,
      ),
    );
  }
}

该应用现已完成创建,但尚未经过测试。

a3c16fc17be25f6c.png 运行应用。它显示的界面将如以下屏幕截图所示:

b74f843e42a28b0f.png

该应用会显示内容列表。点按任意一行上的心形图标,即可填满心形图标并将该内容添加到收藏夹列表。点击 AppBar 上的 Favorites 按钮可进入另一个包含收藏夹列表的屏幕。

该应用现在即可进行测试。您将在下一步中开始测试该应用。

5. 对提供程序进行单元测试

首先,对 favorites 模型进行单元测试。什么是单元测试?单元测试可验证软件的每个单元(例如函数、对象或 widget)能否正确执行其预期任务。

Flutter 应用中的所有测试文件(集成测试除外)都位于 test 目录下。

移除 test/widget_test.dart

a3c16fc17be25f6c.png 在开始测试之前,请删除 widget_test.dart 文件。您将添加自己的测试文件。

创建新的测试文件

首先,您需要在 Favorites 模型中测试 add() 方法,以验证是否已将新内容添加到该列表中,并且该列表反映了相应更改。按照惯例,test 目录下的目录结构与 lib 目录下的一致,Dart 文件的名称相同,但以 _test 结尾。

a3c16fc17be25f6c.pngtest 目录下创建一个 models 目录。在这个新目录下,创建一个包含以下内容的 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. widget 测试

在这一步,您将添加代码以测试 widget。widget 测试对于 Flutter 是唯一的,您可以单独对每个 widget 进行测试。这一步将分别测试 HomePageFavoritesPage 屏幕。

widget 测试使用 testWidget() 函数,而不是 test() 函数。与 test() 函数一样,testWidget() 函数使用两个参数:description,callback,但是回调使用 WidgetTester 作为其参数。

widget 测试使用 TestFlutterWidgetsBinding 类;这个类可为您的 widget 提供与运行中应用相同的资源(例如屏幕尺寸相关信息以及动画调度功能等),但没有在应用内运行。相反,您需要使用虚拟环境来实例化 widget,然后运行测试并获得结果。在此示例中,pumpWidget 启动此流程的方式是让框架装载和测量一个特定 widget,就像在应用中所做的那样。

widget 测试框架提供了用于查找 widget 的查找器,例如 text()byType()byIcon().。该框架还提供匹配器来验证结果。

首先测试 HomePage widget。

创建新的测试文件

第一个测试可验证滚动 HomePage 的操作能否正常执行。

a3c16fc17be25f6c.pngtest 目录下创建一个新文件,将其命名为 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 widget 需要这两个 widget 在 widget 树中位于它的上方,以便从这两个 widget 那里继承并获取对它们所提供的数据的访问权限。此函数会作为参数传递给 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!

您还可以使用设备或模拟器来运行 widget 测试,这样您可以观察测试的运行情况,也可以使用热重启功能。

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 向测试 HomePage widget 的组添加更多测试。将以下测试复制到您的文件中:

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、Protractor、Espresso 或 Earl Gray 版本。该软件包在内部使用 flutter_driver 来驱动在设备上进行该测试。

在 Flutter 中编写集成测试与编写 widget 测试类似,不同之处在于,集成测试在称为目标设备的移动设备、浏览器或桌面应用上运行。

编写测试

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 驱动程序对应用性能进行测试

编写性能测试

在 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 目录包含两个文件:

  1. scrolling_summary.timeline_summary.json 包含摘要。使用任何文本编辑器打开该文件,以查看其中包含的信息。
  2. scrolling_summary.timeline.json 包含完整的时间轴数据。

如需详细了解集成测试,请访问:

9. 恭喜!

您已完成此 Codelab,并学习了测试 Flutter 应用的不同方式。

您学到的内容

  • 如何在单元测试的帮助下测试提供程序
  • 如何使用 widget 测试框架来测试 widget
  • 如何使用集成测试来测试应用的界面
  • 如何使用集成测试来测试应用的性能

如需详细了解如何在 Flutter 中进行测试,请访问