Flutter手动小部件测试插件flutter_manual_widget_tester的使用

Flutter 手动小部件测试插件 flutter_manual_widget_tester 的使用

flutter_manual_widget_tester 是一个 Flutter 包,它允许你在隔离环境中手动测试你的 Flutter 小部件。它提供了一个简单的界面来与你的小部件交互并修改其属性。

开始使用

假设我们正在开发一个名为 CustomList 的小部件,它展示了一串项目列表。它接受一个字符串列表和一个标题字符串作为参数,并根据提供的颜色集对标题进行样式设置。首先,我们编写第一个版本的代码:

class CustomList extends StatelessWidget {
  const CustomList({
    super.key,
    required this.headerColor,
    required this.headingColor,
    required this.stringList,
    required this.heading,
  });

  final Color headerColor;
  final Color headingColor;
  final List<String> stringList;
  final String heading;

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        Container(
          color: headerColor,
          height: 32.0,
          child: Center(
            child: Text(
              heading,
              style: TextStyle(
                color: headingColor,
              ),
            ),
          ),
        ),
        Expanded(
          child: SingleChildScrollView(
            child: Column(
                children: stringList.map((e) {
              return SizedBox(
                height: 32.0,
                child: Text(e),
              );
            }).toList()),
          ),
        ),
      ],
    );
  }
}

现在,为了手动测试这个 CustomList 小部件,我们可以使用由该包提供的 ManualWidgetTester。为此,请确保你的 MyApp 类的 MyHomePage 构建一个 ManualWidgetTester,如下所示:

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  [@override](/user/override)
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key}) : super(key: key);

  [@override](/user/override)
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return ManualWidgetTester(
      builders: [
        WidgetTestBuilder(
          id: 'custom list',
          name: 'Custom List',
          icon: Icons.list,
          builder: (context, settings) {
            final headerColor = settings.getSetting(
              'headerColor',
              const Color.fromRGBO(0, 0, 255, 1.0),
            );
            final headingColor = settings.getSetting(
              'headingColor',
              const Color.fromRGBO(255, 255, 255, 1.0),
            );

            final numberOfItems = settings.getSetting('numberOfItems', 10);
            final stringList = List.generate(
              numberOfItems,
              (index) => 'Item $index',
            );

            final heading = settings.getSetting('heading', 'Custom List');

            return CustomList(
              headerColor: headerColor,
              headingColor: headingColor,
              stringList: stringList,
              heading: heading,
            );
          },
        ),
      ],
    );
  }
}

如你所见,ManualWidgetTester 接收一个 WidgetTestBuilder 实例列表。每个 WidgetTestBuilder 都代表一个要手动测试的小部件。它接收一个在整个热重载过程中保持一致的 ID、一个名称和图标以在界面上显示,最重要的是一个 builder 函数,用于构建要测试的小部件。builder 方法接收一个 WidgetTestSessionCustomSettings 实例,该实例提供了访问可以在界面上修改的设置的方法。在这里,我们使用设置来修改 CustomList 小部件的 headerColorheadingColornumberOfItemsheading 属性。

如果我们现在运行此应用并点击右上角的加号图标,我们将看到我们的 Custom List 小部件在列表中。点击它将加载用于修改其设置并查看小部件的界面。窗口应看起来像这样:

截图

我们可以使用侧边栏来修改小部件的设置,使用底部按钮更改当前缩放级别,使用调整大小句柄调整小部件大小,并像在应用中一样与小部件交互。任何设置的更改都会自动重建小部件并更新视图。例如,我们可以点击 headerColor 按钮并更改颜色以实时查看标题的更新。

截图

此外,我们还可以更改“通用设置”,例如当前媒体查询属性或默认文本样式。

实际上,如果我们增加默认字体大小,我们会发现第一个错误。列表项的高度不足以容纳更大的文本。此外,我们的小部件似乎没有尊重媒体查询中定义的填充。当在应用中测试时,这些错误可能不会被注意到,因为无法手动修改这些通用设置。让我们修复这些错误并执行热重载。代码现在看起来像这样:

class CustomList extends StatelessWidget {
  const CustomList({
    super.key,
    required this.headerColor,
    required this.headingColor,
    required this.stringList,
    required this.heading,
  });

  final Color headerColor;
  final Color headingColor;
  final List<String> stringList;
  final String heading;

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        Container(
          color: headerColor,
          child: SafeArea(
            child: Center(
              child: Text(
                heading,
                style: TextStyle(
                  color: headingColor,
                ),
              ),
            ),
          ),
        ),
        Expanded(
          child: SingleChildScrollView(
            child: Column(
                children: stringList.map((e) {
              return SizedBox(
                child: Text(e),
              );
            }).toList()),
          ),
        ),
      ],
    );
  }
}

更重要的是,自定义列表小部件现在尊重媒体查询并且有足够的高度容纳更大的文本:

截图

完整示例

以下是一个完整的示例,展示了如何使用 flutter_manual_widget_tester 包来测试多个小部件:

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

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  [@override](/user/override)
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key}) : super(key: key);

  [@override](/user/override)
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  [@override](/user/override)
  Widget build(BuildContext context) {
    final builders = [
      WidgetTestBuilder(
        id: 'some key',
        name: 'SomeName',
        icon: Icons.access_alarms_rounded,
        builder: (context, settings) {
          final backgroundColor =
              settings.getSetting('backgroundColor', Colors.white);
          return Container(
            color: backgroundColor,
            child: const Text('foobar2'),
          );
        },
      ),
      WidgetTestBuilder(
        id: 'some other key',
        name: 'SomeOtherName',
        icon: Icons.kayaking,
        builder: (context, settings) {
          final backgroundColor =
              settings.getSetting('backgroundColor', Colors.orange);
          return Container(
            color: backgroundColor,
            child: const Text('foobar6'),
          );
        },
      ),
      WidgetTestBuilder(
        id: 'list view',
        name: 'ListView',
        icon: Icons.list,
        builder: (context, settings) {
          final backgroundColor =
              settings.getSetting('backgroundColor', Colors.white);
          final constrainedInt = settings
              .getSetting(
                  'constrainedInt',
                  ConstrainedInt(
                      lowerLimit: null, value: 0, upperLimit: null, divisor: 2))
              .value;

          return SafeArea(
            child: Container(
              color: backgroundColor,
              child: Text('list view $constrainedInt'),
            ),
          );
        },
      ),
      WidgetTestBuilder(
        id: 'dialog generator',
        name: 'DialogGenerator',
        icon: Icons.window,
        builder: (context, settings) {
          final backgroundColor =
              settings.getSetting('backgroundColor', Colors.blue);
          return Container(
            color: backgroundColor,
            child: const Text('dialog generator'),
          );
        },
      ),
      WidgetTestBuilder(
        id: 'string editor state',
        name: 'StringEditorState',
        icon: Icons.abc,
        builder: (context, settings) {
          final backgroundColor =
              settings.getSetting('backgroundColor', Colors.lightGreen);
          return Container(
            color: backgroundColor,
            child: const Text('edit this string\'s state'),
          );
        },
      ),
      WidgetTestBuilder(
        id: 'substring editor',
        name: 'SubstringEditor',
        icon: Icons.abc,
        iconColor: Colors.red,
        builder: (context, settings) {
          final backgroundColor =
              settings.getSetting('backgroundColor', Colors.green);
          return Container(
            color: backgroundColor,
            child: const Text('it this str'),
          );
        },
      ),
      WidgetTestBuilder(
        id: 'string editor',
        name: 'StringEditor',
        icon: Icons.abc,
        builder: (context, settings) {
          final backgroundColor =
              settings.getSetting('backgroundColor', Colors.green);
          return Container(
            color: backgroundColor,
            child: const Text('edit this string'),
          );
        },
      ),
      WidgetTestBuilder(
        id: 'offset button test',
        name: 'Offset Button Test',
        icon: Icons.smart_button,
        builder: (context, settings) {
          final offsetX = settings.getSetting('offsetX', 0.0);
          final offsetY = settings.getSetting('offsetY', 0.0);

          return Center(
            child: Container(
              color: const Color.fromRGBO(0, 0, 0, 0.25),
              child: SizedBox(
                width: 128.0,
                height: 32.0,
                child: LayoutBuilder(builder: (context, constraints) {
                  return Transform.translate(
                    offset: Offset(offsetX * constraints.maxWidth,
                        offsetY * constraints.maxHeight),
                    child: ElevatedButton(
                      onPressed: () {
                        print('pressed');
                      },
                      child: const Text('button'),
                    ),
                  );
                }),
              ),
            ),
          );
        },
      ),
      WidgetTestBuilder(
        id: 'image list with material app',
        name: 'Image List with Material App',
        icon: Icons.image,
        builder: (context, settings) {
          final numberOfImages = settings.getSetting('numberOfImages', 1);
          final imagePadding = settings
              .getSetting(
                  'imagePadding', ClampedDouble(value: 8.0, lowerLimit: 0.0))
              .value;

          return MaterialApp(
            theme: ThemeData.light(),
            darkTheme: ThemeData.dark(),
            home: Scaffold(
              appBar: AppBar(
                title: const Text('Image List'),
              ),
              body: ImageList(
                numberOfImages: numberOfImages,
                imagePadding: imagePadding,
              ),
            ),
          );
        },
      ),
      WidgetTestBuilder(
        id: 'image list',
        name: 'Image List',
        icon: Icons.image,
        builder: (context, settings) {
          final numberOfImages = settings.getSetting('numberOfImages', 1);
          final imagePadding = settings
              .getSetting(
                  'imagePadding', ClampedDouble(value: 8.0, lowerLimit: 0.0))
              .value;

          return ImageList(
            numberOfImages: numberOfImages,
            imagePadding: imagePadding,
          );
        },
      ),
      WidgetTestBuilder(
        id: 'no custom settings',
        name: 'No Custom Settings',
        icon: Icons.settings,
        builder: (context, settings) {
          return const Text('This widget has no custom settings.');
        },
      ),
      WidgetTestBuilder(
        id: 'radio button test',
        name: 'Radio Button Test',
        icon: Icons.settings,
        builder: (context, settings) {
          final boolean = settings.getSetting('boolean', false);

          return Text('Boolean state: $boolean');
        },
      ),
    ];

    return ManualWidgetTester(
      themeData: ManualWidgetTesterThemeData.fromThemeGeneratorParameters(
          ThemeGeneratorParameters(
        backgroundColor: Colors.blue,
        primaryColor: Colors.orange,
        brightness: Brightness.light,
        animationSpeed: AnimationSpeed.slow,
        designLanguage: DesignLanguage.skeuomorphic,
        layout: Layout.compact,
      )),
      builders: builders,
    );
  }
}

更多关于Flutter手动小部件测试插件flutter_manual_widget_tester的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

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


flutter_manual_widget_tester 是一个用于 Flutter 的手动小部件测试插件,它允许开发者在手动测试 Flutter 应用时,模拟用户交互并验证小部件的状态。这个插件通常用于在开发过程中手动测试 UI 组件,而不是自动化测试。

安装

首先,你需要在 pubspec.yaml 文件中添加 flutter_manual_widget_tester 依赖:

dev_dependencies:
  flutter_manual_widget_tester: ^0.1.0

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

基本用法

  1. 导入包: 在你的测试文件中导入 flutter_manual_widget_tester 包:

    import 'package:flutter_manual_widget_tester/flutter_manual_widget_tester.dart';
    
  2. 创建测试环境: 使用 ManualWidgetTester 类来创建一个测试环境。你可以在这个环境中手动测试你的小部件。

    void main() {
      testWidgets('Manual Widget Test', (WidgetTester tester) async {
        // 创建 ManualWidgetTester 实例
        final manualTester = ManualWidgetTester(tester);
    
        // 构建你的小部件
        await manualTester.pumpWidget(MyWidget());
    
        // 手动测试代码
        // 例如,点击按钮并验证状态
        await manualTester.tap(find.byType(Button));
        await manualTester.pump();
    
        // 验证小部件的状态
        expect(find.text('Button Clicked'), findsOneWidget);
      });
    }
    
  3. 模拟用户交互: 使用 ManualWidgetTester 提供的方法来模拟用户交互,例如点击、滑动等。

    await manualTester.tap(find.byType(Button));
    await manualTester.drag(find.byType(ListView), const Offset(0.0, -200.0));
    
  4. 验证小部件状态: 使用 expect 函数来验证小部件的状态是否符合预期。

    expect(find.text('Button Clicked'), findsOneWidget);
    
  5. 手动刷新小部件: 使用 pumppumpAndSettle 来手动刷新小部件。

    await manualTester.pump();
    await manualTester.pumpAndSettle();
    

示例

以下是一个完整的示例,展示了如何使用 flutter_manual_widget_tester 来手动测试一个按钮点击后的状态变化:

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_manual_widget_tester/flutter_manual_widget_tester.dart';

void main() {
  testWidgets('Button Click Test', (WidgetTester tester) async {
    final manualTester = ManualWidgetTester(tester);

    // 构建小部件
    await manualTester.pumpWidget(MaterialApp(
      home: Scaffold(
        body: Center(
          child: MyButton(),
        ),
      ),
    ));

    // 验证初始状态
    expect(find.text('Click Me'), findsOneWidget);
    expect(find.text('Button Clicked'), findsNothing);

    // 模拟按钮点击
    await manualTester.tap(find.byType(ElevatedButton));
    await manualTester.pump();

    // 验证点击后的状态
    expect(find.text('Click Me'), findsNothing);
    expect(find.text('Button Clicked'), findsOneWidget);
  });
}

class MyButton extends StatefulWidget {
  [@override](/user/override)
  _MyButtonState createState() => _MyButtonState();
}

class _MyButtonState extends State<MyButton> {
  bool _clicked = false;

  [@override](/user/override)
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () {
        setState(() {
          _clicked = true;
        });
      },
      child: Text(_clicked ? 'Button Clicked' : 'Click Me'),
    );
  }
}
回到顶部