Flutter蛋糕定制插件cake_flutter的使用

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

Flutter蛋糕定制插件cake_flutter的使用

Cake Tester Logo

Cake 测试运行器用于 Flutter

Pub Version License: MPL 2.0

专为 Flutter 构建的最小单元测试工具!如果只需要 Dart 的基础包,请获取基础 Cake 包。

在 VS Code 中运行此工具?安装 Cake VS-Code 扩展 以在 IDE 中运行测试、代码片段等。

警告:Beta 版

  • 此软件包目前处于 Beta 阶段。文档稀缺,功能可能会有变动,存在大量错误。请自行承担风险。

入门指南

安装

  1. 全局安装 Cake 测试运行器。
dart pub global activate cake
  1. (可选)安装 VSCode 扩展

  2. cake_flutter 包添加到您的 开发依赖项。(这还包括了 cake 包。)

flutter pub add dev:cake_flutter
  1. 在您的 开发依赖项 中包含 flutter_test 包。
dev_dependencies:
  flutter_test:
    sdk: flutter

运行

  • Cake 会搜索在运行目录中具有 .cake.dart 后缀的文件。

通过命令行使用 Cake 运行

在要运行测试的目录中执行以下命令:

dart run cake

您还可以添加标志来运行特定测试或查看特定测试的输出。

通过 Flutter 测试命令行运行

Cake-Flutter 是基于现有的 Flutter 测试器构建的,因此可以使用 flutter test 命令运行 Cake 测试。虽然如此,如果您坚持使用 .cake.dart 命名约定,则必须单独调用每个文件。 在要运行测试的目录中执行以下命令:

flutter test path/to/your/test.cake.dart

使用 VS Code

  • 在测试浏览器中或直接在文件中运行或调试测试。(有关更多详细信息,请参阅 市场页面。)

编写单元测试

让我们从一个快速示例开始。这是 Cake 相当于 模板 Flutter 项目测试的等效物。

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

void main() {
  FlutterTestRunner(
    'Counter increments smoke test',
    [
      Test(
        'Counter should start at zero',
        action: (test) => test.index(),
        assertions: (test) => [
          Expect.equals(actual: test.search.text('0').length, expected: 1),
          Expect.equals(actual: test.search.text('1').length, expected: 0),
        ],
      ),
      Test(
        'Counter should increment when + is tapped',
        action: (test) async {
          test.index();
          await test.search.icon(Icons.add).first.tap();
          await test.forward();
          test.index();
        },
        assertions: (test) => [
          Expect.equals(actual: test.search.text('0').length, expected: 0),
          Expect.equals(actual: test.search.text('1').length, expected: 1),
        ],
      ),
    ],
    setup: (test) async {
      await test.setApp(const MyApp());
    },
  );
}

更多示例,请参阅 /examples/test 文件夹。

组织

Cake 依靠层次结构来组织测试。所有测试必须被包裹在一个 TestRunner 中。然后,您可以直接列出 Tests 或进一步将其嵌套到组中。建议每个文件只有一个 TestRunner。

阶段

Cake 鼓励开发者编写清晰、简单的原子测试,通过定义每个测试的独立阶段来实现。您可能熟悉 “Arrange-Act-Assert” 或 “Given-When-Then” 的测试编写风格。在这里,您可以将其类比为 “Setup-Action-Assertions”(如有必要,包括额外的 teardown)。

所有阶段都可以编写为异步的,如果需要的话。

设置与清理

设置和清理是从父级继承到子级测试和组的。因此,您可以在根 TestRunner 中编写初始设置逻辑,然后在特定组或测试中运行任何特定的设置,以便专门安排正在测试的功能。所有设置在任何操作之前运行。所有清理在所有断言完成后运行,并且无论测试过程中是否出现任何问题都会运行。

操作

操作阶段旨在突出显示测试结果的动作。这可能是点击按钮、发送 API 调用或运行函数。请记住,在一次测试中只能运行一个动作。如果您发现操作函数很大或包含大量代码,这通常是一个信号,表明它需要分解为多个测试或放入 setup 函数中。

虽然通常需要,但操作阶段不是必需的。经常验证初始状态的单元测试可能会跳过操作阶段。

操作可以返回一个值,该值可以稍后用于断言。

断言

断言在操作阶段之后运行,并返回一个 Expect 列表。这些断言将预期输出与实际结果进行匹配。断言的数量没有限制。默认情况下,Cake 在第一个失败后忽略其余断言。这可以在 选项 中关闭。

上下文

上下文是如何从阶段传递信息的方式,是一个从父级到子级继承的对象。这作为每个阶段的参数传递。

Flutter 上下文

与基础 Dart Cake 不同,Flutter Cake 有自己的 Flutter 上下文,类似于 flutter_test 包的包装器。

WidgetTester

您可以通过 Flutter 上下文的 tester 属性访问 flutter_test WidgetTester。理想情况下,您不需要这样做,但如果要将测试迁移到 Cake 或作为变通方法,它是可用的。

搜索

像测试运行器中的 find 一样,Cake 测试器也有自己的类似搜索功能,可以爬取并索引您的测试的小部件树。

有效的搜索标准:

  • 按键搜索
  • 按图标搜索
  • 按文本搜索
  • 按小部件类型搜索

索引

搜索需要一些处理来索引,所以您必须首先手动调用 test.index()。一旦索引,您可以调用 test.search 来搜索小部件。您可以添加索引选项以仅索引某些小部件以提高搜索性能,或者启用调试选项以打印到控制台。记住在提交代码前关闭任何调试标志。

快照测试

Cake 支持快照测试。快照是…

断言匹配

通用

  • equals

  • isEqual *

  • isNotEqual

  • isNull

  • isNotNull

  • isType **

  • isTrue

  • isFalse

  • equals 和 isEqual 可以互换使用。

  • isEqual 和 isType 需要定义泛型,否则它们总是通过,因为它们认为类型是 dynamic

Flutter 特定

  • isWidgetType *

  • searchHasNone

  • searchHasOne

  • searchHasSome

  • searchHasN

  • findMatch

  • findsOneWidget

  • findsNothing

  • findsWidgets

  • findsNWidgets

  • findsAtLeastNWidgets

  • snapshotMatches

  • snapshotsMatch

  • matchesGolden

  • snapshotIsEqual

  • snapshotIsNotEqual

  • isWidgetType 需要定义泛型,否则它们总是通过,因为它们认为类型是 Widget

使用示例

  1. 对整个设置的小部件进行简单的快照。
Test(
  'Counter should start at zero',
  action: (test) async {
    await test.snapshot(fileName: 'initial_state');
  },
  assertions: (test) => [
    SnapshotExpect.snapshotMatches(test.snapshots.first),
  ],
)
  1. 对符合标准的多个小部件进行快照。
Test(
  'Can take a snapshot of multiple widgets',
  action: (test) async {
    await test.search.textIncludes('').snapshot(
          fileName: 'multiple_widgets',
          // 因为我们只对文本小部件进行快照而不是其父级支架,所以我们需要指定文本方向。
          snapshotWidgetSetup:
              const SetupSettings(textDirection: TextDirection.ltr),
        );
  },
  assertions: (test) => [
    // 注意,调用集合的快照将只返回一个快照。这里只是为了展示检查快照的不同方式。
    ...SnapshotExpect.snapshotsMatch(test.snapshots),
  ],
),

快照选项

快照选项允许控制快照的渲染方式、如何处理不匹配以及应保存的位置。这些可以为每个快照单独设置,也可以为运行器、组或测试设置。

  1. includeSetupWidgets
  • 类型: bool?
  • 默认: false
  • 描述:
  • 确定是否包含快照中的设置小部件。
  • 如果为 false,快照将仅包括根小部件。
  • 使用:
test.snapshot(includeSetupWidgets: true);
  1. createIfMissing
  • 类型: bool?
  • 默认: true
  • 描述:
  • 创建快照,如果它不存在。
  • 在第一次测试运行期间初始化快照很有用。
  • 使用:
test.snapshot(createIfMissing: false);
  1. createCopyIfMismatch
  • 类型: bool?
  • 默认: true
  • 描述:
  • 如果快照不匹配,则创建不匹配快照的副本。
  • 有助于通过保留当前状态进行比较来调试更改。
  • 使用:
test.snapshot(createCopyIfMismatch: true);
  1. mismatchDirectory 和 mismatchFileName
  • 类型: String?
  • 默认: 在快照的目录和文件名后附加 _mismatch。
  • 描述:
  • 指定保存不匹配快照的位置。
  • 有助于在专用目录中组织不匹配的快照。
  • 使用:
test.snapshot(
  mismatchDirectory: 'test/snapshots/mismatches',
  mismatchFileName: 'my_widget_mismatch',
);
  1. overwriteGolden
  • 类型: bool?
  • 默认: false
  • 描述:
  • 如果存在则覆盖当前黄金文件。
  • 有意在更改后更新快照很有用。
  • 使用:
test.snapshot(overwriteGolden: true);
  1. directory 和 fileName
  • 类型: String?
  • 默认:
  • directory: “test/snapshots”
  • fileName: 从测试名称派生。
  • 描述:
  • 指定快照文件的位置和名称。
  • 使用:
test.snapshot(
  directory: 'custom/snapshots',
  fileName: 'my_widget',
);
  1. fontFamily
  • 类型: String?
  • 默认: 将使用 Flutter 的默认测试字体。
  • 描述:
  • 指定快照中使用的字体家族。
  • 如果使用自定义字体,请先加载它并将族名传递到这里。
  • 使用:
test.snapshot(fontFamily: 'Roboto');
  1. warnIfInvalid
  • 类型: bool?
  • 默认: 未明确记录。
  • 描述:
  • 如果快照无效或不可比较,则显示警告。
  • 使用:
test.snapshot(warnIfInvalid: true);
  1. snapshotWidget
  • 类型: Widget?
  • 描述:
  • 捕获特定小部件的快照而不是整个测试环境。
  • 使用:
test.snapshot(snapshotWidget: MyCustomWidget());
  1. snapshotWidgetSetup
  • 类型: SetupSettings?
  • 描述:
  • 提供额外的配置给 snapshotWidget,例如文本方向。
  • 使用:
test.snapshot(
  snapshotWidget: MyCustomWidget(),
  snapshotWidgetSetup: const SetupSettings(textDirection: TextDirection.ltr),
);

示例全使用:

test.snapshot(
  includeSetupWidgets: false,
  createIfMissing: true,
  createCopyIfMismatch: true,
  mismatchDirectory: 'test/snapshots/mismatches',
  mismatchFileName: 'my_widget_mismatch',
  overwriteGolden: false,
  directory: 'test/snapshots',
  fileName: 'my_widget',
  fontFamily: 'Roboto',
  warnIfInvalid: true,
  snapshotWidget: MyCustomWidget(),
  snapshotWidgetSetup: const SetupSettings(textDirection: TextDirection.ltr),
);

关于快照字体

根据版本,Flutter 将使用 “Ahem”(3.7 之前的版本)或 “FlutterTest”(3.7 或更高版本)。如果您注意到快照中的字体呈现为块状,则这是默认设置。Flutter 这样做是为了确保个别字体更改不会妨碍快照测试。

然而,这并不总是期望的效果。要覆盖它,请在加载小部件之前加载所需的字体。这是一个非常简单的示例,您可以根据需要修改加载方式。

import 'package:cake_flutter/cake_flutter.dart';

void main() {
  FlutterTestRunner(
    'Counter Snapshots',
    [
      Test(
        'Counter should start at zero',
        action: (test) async {
          await test.snapshot();
        },
        assertions: (test) => [
          ...SnapshotExpect.snapshotMatch(test.snapshots),
        ],
      ),
    ],
    setup: (test) async {
      // 为测试设置自定义字体
      final fontData = rootBundle.load('assets/fonts/Roboto-Regular.ttf');
      final fontLoader = FontLoader('Roboto')..addFont(fontData);
      await fontLoader.load();

      await test.setApp(const MyApp());
    },
    options: const FlutterTestOptions(
      snapshotOptions: SnapshotOptions(
        // 设置此选项以便在此测试运行器中使用的每个快照。
        // 这也可以在调用 .snapshot() 时单独设置。
        fontFamily: 'Roboto',
      ),
    ),
  );
}

更多关于Flutter蛋糕定制插件cake_flutter的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter蛋糕定制插件cake_flutter的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,下面是一个关于如何在Flutter项目中使用cake_flutter插件的示例代码。假设cake_flutter插件提供了一些用于蛋糕定制的功能,比如选择蛋糕尺寸、口味、装饰等。请注意,实际插件的功能和API可能会有所不同,以下代码仅为示例,具体实现需参考插件的官方文档。

首先,确保你已经在pubspec.yaml文件中添加了cake_flutter插件的依赖:

dependencies:
  flutter:
    sdk: flutter
  cake_flutter: ^latest_version  # 替换为插件的最新版本号

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

接下来,在你的Flutter项目中,你可以按照以下方式使用cake_flutter插件:

import 'package:flutter/material.dart';
import 'package:cake_flutter/cake_flutter.dart'; // 假设这是插件的导入路径

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Cake Customization App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: CakeCustomizationScreen(),
    );
  }
}

class CakeCustomizationScreen extends StatefulWidget {
  @override
  _CakeCustomizationScreenState createState() => _CakeCustomizationScreenState();
}

class _CakeCustomizationScreenState extends State<CakeCustomizationScreen> {
  CakeCustomizationModel _cakeModel = CakeCustomizationModel(); // 假设这是插件提供的模型类

  void _onSizeChanged(CakeSize size) {
    setState(() {
      _cakeModel.size = size;
    });
  }

  void _onFlavorChanged(CakeFlavor flavor) {
    setState(() {
      _cakeModel.flavor = flavor;
    });
  }

  void _onDecorationChanged(List<CakeDecoration> decorations) {
    setState(() {
      _cakeModel.decorations = decorations;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Customize Your Cake'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text('Cake Size:', style: TextStyle(fontSize: 18)),
            SizedBox(height: 8),
            DropdownButton<CakeSize>(
              value: _cakeModel.size,
              hint: Text('Select Size'),
              onChanged: _onSizeChanged,
              items: CakeSize.values.map<DropdownMenuItem<CakeSize>>((CakeSize value) {
                return DropdownMenuItem<CakeSize>(
                  value: value,
                  child: Text(value.toString()),
                );
              }).toList(),
            ),
            SizedBox(height: 24),
            Text('Cake Flavor:', style: TextStyle(fontSize: 18)),
            SizedBox(height: 8),
            DropdownButton<CakeFlavor>(
              value: _cakeModel.flavor,
              hint: Text('Select Flavor'),
              onChanged: _onFlavorChanged,
              items: CakeFlavor.values.map<DropdownMenuItem<CakeFlavor>>((CakeFlavor value) {
                return DropdownMenuItem<CakeFlavor>(
                  value: value,
                  child: Text(value.toString()),
                );
              }).toList(),
            ),
            SizedBox(height: 24),
            Text('Cake Decorations:', style: TextStyle(fontSize: 18)),
            SizedBox(height: 8),
            Wrap(
              children: _cakeModel.decorations.map<Widget>((CakeDecoration decoration) {
                return Chip(
                  label: Text(decoration.toString()),
                  onDeleted: () {
                    setState(() {
                      _cakeModel.decorations.remove(decoration);
                    });
                  },
                );
              }).toList(),
            ),
            SizedBox(height: 16),
            ElevatedButton(
              onPressed: () {
                // 提交蛋糕定制信息,这里可以调用插件提供的API进行下一步操作
                // 例如:CakeCustomizationService.submitCustomization(_cakeModel);
                print('Cake Model: $_cakeModel');
              },
              child: Text('Submit'),
            ),
          ],
        ),
      ),
    );
  }
}

// 假设这些是插件定义的枚举类,实际使用时需要参考插件的文档
enum CakeSize { small, medium, large }
enum CakeFlavor { chocolate, vanilla, strawberry }
class CakeDecoration {
  final String name;

  CakeDecoration(this.name);

  @override
  String toString() => name;
}

// 假设这是插件提供的模型类,用于存储蛋糕定制信息
class CakeCustomizationModel {
  CakeSize size = CakeSize.small;
  CakeFlavor flavor = CakeFlavor.chocolate;
  List<CakeDecoration> decorations = [];

  @override
  String toString() {
    return 'CakeCustomizationModel{size: $size, flavor: $flavor, decorations: $decorations}';
  }
}

在这个示例中,我们创建了一个简单的蛋糕定制界面,用户可以选择蛋糕的尺寸、口味和装饰。这些信息被存储在一个CakeCustomizationModel对象中。请注意,由于cake_flutter插件的具体API和功能未知,上述代码中的枚举类、模型类和插件调用都是假设的。实际使用时,你需要参考插件的官方文档来调整代码。

回到顶部