Flutter测试辅助插件surf_widget_test_composer的使用

发布于 1周前 作者 htzhanglong 来自 Flutter

Flutter测试辅助插件 surf_widget_test_composer 的使用

surf_widget_test_composer 是一个由 Surf 团队开发的实用工具包,旨在简化 Flutter 应用中的小部件和黄金测试(Golden Tests)过程。它基于 golden_toolkit 包构建,提供了丰富的功能来促进高效的测试工作流程。

概述

该插件可以帮助开发者通过配置文件轻松设置本地化、主题、设备等参数,并自动生成多种场景下的黄金图像(Goldens),从而简化了 UI 测试的过程。

安装

在你的 pubspec.yaml 文件中添加 surf_widget_test_composer 依赖:

dependencies:
  surf_widget_test_composer: $currentVersion$

请确保将 $currentVersion$ 替换为最新版本号。

示例 Demo

初始化配置

首先,你需要创建一个名为 test/flutter_test_config.dart 的文件,在其中指定应用的本地化、主题、测试设备列表以及黄金测试的容差值。

假设你有两个主题(浅色和深色)、三种设备(iPhone 11、Google Pixel 4a 和 iPhone SE 1)、两种语言(英语和俄语)。你的 flutter_test_config.dart 文件可能如下所示:

import 'package:surf_widget_test_composer/surf_widget_test_composer.dart' as helper;

/// Localization and locales from auto-generated AppLocalizations.
const _localizations = AppLocalizations.localizationsDelegates;
const _locales = AppLocalizations.supportedLocales;

Future<void> testExecutable(FutureOr<void> Function() testMain) {
  /// Define themes for testing.
  final themes = [
    helper.TestingTheme(
      data: ThemeData.dark(),
      stringified: 'dark',
      type: helper.ThemeType.dark,
    ),
    helper.TestingTheme(
      data: ThemeData.light(),
      stringified: 'light',
      type: helper.ThemeType.light,
    ),
  ];

  /// Define devices for testing.
  final devices = [
    helper.TestDevice(
      name: 'iphone11',
      size: const Size(414, 896),
      safeArea: const EdgeInsets.only(top: 44, bottom: 34),
    ),
    helper.TestDevice(
      name: 'pixel 4a',
      size: const Size(393, 851),
    ),
    helper.TestDevice(
      name: 'iphone_se_1',
      size: const Size(640 / 2, 1136 / 2),
    ),
  ];

  return helper.testExecutable(
    testMain: testMain,
    themes: themes,
    localizations: _localizations,
    locales: _locales,
    wrapper: (child, mode, theme, localizations, locales) =>
        helper.BaseWidgetTestWrapper(
      childBuilder: child,
      mode: mode,
      themeData: theme,
      localizations: localizations,
      localeOverrides: locales,
      dependencies: (child) => child,
    ),
    backgroundColor: (theme) => theme.colorScheme.background,
    devicesForTest: devices,
    tolerance: 0.5,
  );
}

根据上述配置,每个测试将会生成 12 张黄金图片:2 种语言 × 2 种主题 × 3 台设备。

使用示例

基本使用

如果你需要进行除了黄金测试之外的小部件测试,可以参考以下代码:

class MockSettingsService extends Mock implements SettingsService {}

void main() {
  final mockSettingsService = MockSettingsService();

  const widget = SettingsScreen();

  /// Generate golden.
  testWidget<SettingsScreen>(
    desc: 'SettingsScreen',
    widgetBuilder: (context, theme) => ProviderScope(
      overrides: [
        settingsServiceProvider.overrideWithValue(mockSettingsService),
      ],
      child: Consumer(
        builder: (context, ref, _) => widget.build(context, ref),
      ),
    ),
    setup: (context, mode) {
      registerFallbackValue(ThemeMode.light);

      when(() => mockSettingsService.themeMode()).thenAnswer(
        (_) => Future.value(ThemeMode.dark),
      );
      when(() => mockSettingsService.updateThemeMode(any()))
          .thenAnswer((_) => Future.value());
    },
    test: (tester, context) async {
      final button = find.byType(DropdownButton<ThemeMode>);
      expect(button, findsOneWidget);

      final floatingActionButton = find.byIcon(Icons.light_mode);
      expect(floatingActionButton, findsOneWidget);

      verifyNever(() => mockSettingsService.updateThemeMode(any()));
      await tester.tap(floatingActionButton);
      verify(() => mockSettingsService.updateThemeMode(any())).called(1);
      await tester.pumpAndSettle();

      expect(find.byIcon(Icons.mode_night), findsOneWidget);
    },
  );
}

纯黄金测试

如果你只需要生成黄金图片而不需要执行其他测试,可以这样写:

void main() {
  const widget = SampleItemListView();

  /// Generate golden.
  testWidget<SampleItemListView>(
    desc: 'SampleItemListView - result',
    widgetBuilder: (context, _) => widget.build(context),
    screenState: 'result', // Optional: specify the state of the widget
  );
}

Riverpod 示例

对于使用 Riverpod 状态管理的应用,你可以这样编写测试:

class MockRiverpodCounterScreenController extends AutoDisposeNotifier<int>
    with Mock
    implements RiverpodCounterScreenController {}

void main() {
  const int testValue = 5;
  const widget = RiverpodCounterScreen();
  final mockController = MockRiverpodCounterScreenController();

  final container = ProviderContainer(
    overrides: [
      riverpodCounterScreenControllerProvider.overrideWith(() => mockController),
    ],
  );

  /// Generate golden.
  testWidget<RiverpodCounterScreen>(
    desc: 'RiverpodCounterScreen',
    widgetBuilder: (context, theme) => UncontrolledProviderScope(
      container: container,
      child: Consumer(
        builder: (context, ref, _) => widget.build(context, ref),
      ),
    ),
    setup: (context, mode) {
      when(() => mockController.build()).thenReturn(testValue);
      when(() => mockController.increment()).thenReturn(null);
    },
    test: (tester, context) async {
      expect(find.widgetWithText(Center, testValue.toString()), findsOneWidget);

      final floatingActionButton = find.byIcon(Icons.add);
      expect(floatingActionButton, findsOneWidget);

      await tester.tap(floatingActionButton);
      verify(() => mockController.increment()).called(1);
    },
  );
}

生成黄金图片

在运行测试之前,记得更新黄金图片:

flutter test --update-goldens --tags=golden

注意事项

  • 在测试时,如果遇到动画导致的无限循环问题,可以通过定义自定义泵函数或在特定环境下调整动画行为来解决。

  • 总是明确指定测试小部件的泛型类型(例如 testWidget<TestableScreen>),因为黄金图片的命名是基于小部件类名生成的。

以上就是关于 surf_widget_test_composer 插件的基本使用方法及其示例。希望这些信息能帮助你更好地进行 Flutter 应用的测试!


更多关于Flutter测试辅助插件surf_widget_test_composer的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter测试辅助插件surf_widget_test_composer的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,以下是如何在Flutter项目中使用surf_widget_test_composer插件进行UI测试的示例代码。surf_widget_test_composer是一个强大的Flutter测试辅助插件,它允许开发者以交互方式构建和测试他们的UI组件。

首先,确保你已经将surf_widget_test_composer添加到你的pubspec.yaml文件中:

dependencies:
  flutter:
    sdk: flutter
  surf_widget_test_composer: ^x.y.z  # 替换为最新版本号

然后,运行flutter pub get来安装依赖。

使用surf_widget_test_composer进行测试

以下是一个简单的Flutter Widget,以及如何使用surf_widget_test_composer来测试它的示例。

1. 创建一个简单的Widget

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

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('My Widget'),
      ),
      body: Center(
        child: Text('Hello, World!'),
      ),
    );
  }
}

2. 编写测试代码

在你的test文件夹中,创建一个新的测试文件,例如my_widget_test.dart

// my_widget_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:surf_widget_test_composer/surf_widget_test_composer.dart';
import 'package:your_app/my_widget.dart';  // 替换为你的实际路径

void main() {
  testWidgets('test MyWidget using surf_widget_test_composer', (WidgetTester tester) async {
    // 初始化Composer
    final composer = await SurfComposer.init(
      tester: tester,
      initialWidget: MaterialApp(home: MyWidget()),
    );

    // 查找AppBar的标题并验证其内容
    await composer
      .findByText('My Widget', matcher: containsText('My Widget'))
      .verify();

    // 查找Center中的Text并验证其内容
    await composer
      .findByText('Hello, World!', matcher: containsText('Hello, World!'))
      .verify();

    // 你可以添加更多的交互和验证步骤,例如点击、滑动等
    // await composer.findByType(ElevatedButton).tap(); // 假设有一个按钮

    // 关闭Composer
    await composer.close();
  });
}

解释

  1. 初始化ComposerSurfComposer.init方法用于初始化Composer并设置初始Widget。在这个例子中,我们设置了一个包含MyWidgetMaterialApp

  2. 查找和验证:使用composer.findByText方法查找包含特定文本的Widget,并使用verify方法验证其存在和内容。

  3. 交互:你可以使用tapscroll等方法来模拟用户交互。在上面的例子中,我们注释掉了一个假设的按钮点击操作。

  4. 关闭Composer:完成测试后,调用composer.close()方法关闭Composer。

这个示例展示了如何使用surf_widget_test_composer来查找和验证Widget的存在和内容。你可以根据需要添加更多的交互和验证步骤来构建更复杂的测试。

回到顶部