Flutter集成测试插件gherkin_integration_test的使用

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

Flutter集成测试插件gherkin_integration_test的使用

🦾 Gherkin Integration Test

本插件基于名为Gherkin的**行为驱动开发(Behavior Driven Development)**语言。这种语言使我们作为开发者能够以直观且易读的方式设计和执行测试。对于不太有开发经验的人来说,这些测试也容易理解,因为其语法非常接近英语。

在Gherkin中,大多数测试看起来像这样:

Feature: This feature shows an example

    Scenario: It shows a good example
      Given we start without an example
      When an example gets created
      Then the example should explode

通过这种方式,我们构建了我们的框架,并可以使用以下类:

  • IntegrationTest
  • IntegrationFeature
  • IntegrationScenario
  • IntegrationExample
  • IntegrationStep(抽象)
    • Given
    • When
    • Then
    • And
    • But

从上到下,每个类可能包含一个或多个下层类。IntegrationTest 可能包含多个 IntegrationFeature,后者又可能包含多个 IntegrationScenario,后者又可能包含多个 IntegrationExampleIntegrationStep

🛠 实现

首先创建一个继承自 IntegrationTest 类的测试类。然后创建一个不带参数的构造函数,但需要调用带有 description 和当前为空的 features 列表的超类。

class DummyIntegrationTest extends IntegrationTest {
  DummyIntegrationTest()
      : super(
          description: '所有与dummies相关的集成测试',
          features: [],
        );
}

📲 特性

features 列表中,我们现在可以定义第一个 IntegrationFeature。给它一个名称和一个空的 scenarios 列表。

class DummyIntegrationTest extends IntegrationTest {
  DummyIntegrationTest()
      : super(
          description: '所有与dummies相关的集成测试',
          features: [
            IntegrationFeature(
              description: '保存dummies',
              scenarios: [],
            ),
          ],
        );
}

🤝 场景

现在考虑一下在你的测试中可能会出现哪些 scenarios。例如,我们可以使用 “成功保存” 和 “失败保存” 作为可能的 scenarios

我们使用 IntegrationScenario 类来创建这两个 scenarios 并将它们放在空列表中。我们还传递一个 description 和这次是一个空的 steps 列表。

class DummyIntegrationTest extends IntegrationTest {
  DummyIntegrationTest()
      : super(
          description: '所有与dummies相关的集成测试',
          features: [
            IntegrationFeature(
              description: '保存dummies',
              scenarios: [
                IntegrationScenario(
                  description: '成功保存好的dummy应该成功',
                  steps: [],
                ),
                IntegrationScenario(
                  description: '保存坏的dummy应该失败',
                  steps: [],
                ),
              ],
            ),
          ],
        );
}

🐾 步骤

现在进入重要部分。对于每个场景,我们可以定义 steps。我们有访问 GivenWhenThenAndBut 的权限。虽然这些步骤在后台基本上做同样的事情,但通过正确使用它们,你可以学会计划、解决和执行测试以一种直观且正确的BDD方式。

每个步骤需要一个描述和一个回调。IntegrationTests 的回调如下所示,并授予访问以下参数的权限:

/// 回调用于提供执行[IntegrationStep]所需的工具。
typedef IntegrationStepCallback<Example extends IntegrationExample?> = FutureOr<void> Function(
  WidgetTester tester,
  IntegrationLog log,
  IntegrationBox box,
  IntegrationMocks mocks, [
  Example? example,
  IntegrationTestWidgetsFlutterBinding? binding,
]);
  • WidgetTester tester

    • 类,用于与小部件和测试环境进行程序化交互(直接来自 Flutter 的 integration_test 包)。
  • Log log

    • 类,允许你在测试中对步骤信息进行微妙的日志记录。
  • IntegrationBox box

    • 这个框基本上是一个映射,可以在一系列步骤内持久保存需要的值。任何你 box.write(key, value) 的值都可以在之后的所有 IntegrationStep 中检索,直到被移除或所有步骤都被执行。使用 box.read(key) 会自动将其转换为你指定的类型。例如,这样读取一个 intfinal int value = box.read(myIntValue) 将自动将其转换为 int
  • IntegrationMocks mocks

    • 一个在整个 IntegrationTestIntegrationFeature 和/或 IntegrationScenario 中存在的持久盒子。你可以选择使用此盒子来存储你需要的模拟,以便稍后检索它们并设置方法。你可以在任何方法中设置模拟,但建议使用 setUpMocks 方法,因为它会在任何其他方法之前运行,这将允许你保持良好的概述。
  • IntegrationExample? example

    • 可选的‘Scenario Outline’示例,可以在 IntegrationScenario 中指定,如下所示:
IntegrationScenario(
  description: '保存好的dummy应该成功',
  examples: [
    const IntegrationExample(values: [1]),
    const IntegrationExample(values: [5]),
    const IntegrationExample(values: [10]),
  ],
)

IntegrationScenario 现在将运行三次,每次运行一个 IntegrationExample。你可以按以下方式访问 example

Given(
    'I access the example value',
    (tester, log, box, mocks, [example, binding]) {
      final int exampleValue = example!.firstValue();
    },
  )

🐾 步骤实现

结合上述信息,我们将成功场景最终设置和配置如下:

class DummyIntegrationTest extends IntegrationTest {
  DummyIntegrationTest()
      : super(
          description: '所有与dummies相关的集成测试',
          features: [
            IntegrationFeature(
              description: '保存dummies',
              setUpOnce: (mocks) {
                final dummyMock = DummyMock();
                mocks.write(dummyMock);
              },
              scenarios: [
                IntegrationScenario(
                  description: '成功保存好的dummy应该成功',
                  steps: [
                    Given(
                      'The dummy service is initialized',
                      (tester, log, box, mocks, [example, binding]) {
                        mocks.read(DummyMock).stubWhatever();
                        // TODO(you): Initialize service
                      },
                    ),
                    When(
                      'We call the dummy service with dummy info',
                      (tester, log, box, mocks, [example, binding]) {
                        // TODO(you): Call dummy service with dummy info
                      },
                    ),
                    Then(
                      'It should succeed',
                      (tester, log, box, mocks, [example, binding]) {
                        // TODO(you): Verify success
                      },
                    ),
                  ],
                ),
                IntegrationScenario(
                  description: '保存坏的dummy应该失败',
                  steps: [],
                ),
              ],
            ),
          ],
        );
}

🏆 额外的集成步骤

由于不是每个人都会以相同的方式编写测试,我们还创建了这些组合步骤类,以便以较少的步骤创建相同的集成测试。

  • GivenWhenThen

    • 当你不希望分别创建和使用有关 ‘Given’、‘When’ 和 ‘Then’ 步骤的功能时,可以使用这个类。这允许你在一个步骤中编写整个测试。
  • WhenThen

    • 当你不希望分别创建和使用有关 ‘When’ 和 ‘Then’ 步骤的功能时,可以使用这个类。这允许你将两个步骤组合成一个。
  • Should

    • 当你觉得使用步骤不符合你的风格时,可以使用这个步骤。这个步骤用一个 ‘Should’ 句子定义了整个测试。

⚡️ 几乎完成!

虽然这可能完全满足我们的测试需求,但我们还有一些功能可供使用,这些功能会给我们的测试增加额外的力量。

🏗 setUpMocks, setUpOnce, setUpEach, tearDownOnce, tearDownEach

每个类都有访问这些方法的权限,并将以类似的方式运行:

  • setUpMocks - 在任何其他方法之前运行。
  • setUpEach - 每个 IntegrationScenario 开始时运行。
  • tearDownEach - 每个 IntegrationScenario 结束时运行。
  • setUpOnce - 在所选类开始时仅运行一次。
  • tearDownOnce - 在所选类结束时仅运行一次。

使用这些方法可能看起来像这样:

class DummyIntegrationTest extends IntegrationTest {
  DummyIntegrationTest()
      : super(
          description: '所有与dummies相关的集成测试',
          features: [
            IntegrationFeature(
              description: '保存dummies',
              setUpMocks: (mocks) {
                mocks.write(DummyMock());
              },
              setUpOnce: (mocks) {
                // Do something once
              },
              setUpEach: (mocks) async {
                AppSetup.reset();
              },
              tearDownOnce: (mocks) async {
                // Do something
              },
              scenarios: [
                IntegrationScenario(
                  description: '成功保存好的dummy应该成功',
                  steps: [
                    Given(
                      'The dummy service is initialized',
                      (tester, log, box, mocks, [example, binding]) {
                        mocks.read(DummyMock).stubWhatever();
                        // TODO(you): Initialize service
                      },
                    ),
                    When(
                      'We call the dummy service with dummy info',
                      (tester, log, box, mocks, [example, binding]) {
                        // TODO(you): Call dummy service with dummy info
                      },
                    ),
                    Then(
                      'It should succeed',
                      (tester, log, box, mocks, [example, binding]) {
                        // TODO(you): Verify success
                      },
                    ),
                  ],
                ),
                IntegrationScenario(
                  description: '保存坏的dummy应该失败',
                  steps: [],
                ),
              ],
            ),
          ],
        );
}

要运行这些测试,只需将 DummyIntegrationTests 添加到主测试函数中并运行即可。在这个例子中,我们希望在测试中使用 IntegrationTestWidgetsFlutterBinding,因此让我们将其添加到构造函数中。

// 添加到构造函数
DummyIntegrationTest({required IntegrationTestWidgetsFlutterBinding binding})
      : super(
          description: '所有与dummies相关的集成测试',
          binding: binding,
        );

void main() {
  // 通过调用此函数获取绑定
  final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized()
      as IntegrationTestWidgetsFlutterBinding;
  // 运行测试
  DummyIntegrationTests(binding: binding).test();
}

示例代码

以下是一个完整的示例代码,展示了如何使用 gherkin_integration_test 插件进行集成测试。

import 'package:example/gherkin_integration_test_view.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:gherkin_integration_test/integration_test.dart';

import 'const_keys.dart';
import 'const_tooltips.dart';

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

/// 仅用于展示包的功能。
///
/// 示例项目 /integration_test 有各种类型的测试,完全测试示例项目。
class IncrementModelCounterScenario extends IntegrationScenario {
  IncrementModelCounterScenario()
      : super(
          description: '递增modelCounter',
          examples: [
            const IntegrationExample(values: [1]),
            const IntegrationExample(values: [5]),
            const IntegrationExample(values: [10]),
          ],
          steps: [
            Given(
              'The GherkinIntegrationTestView is active',
              (tester, log, box, mocks, [example, binding]) async {
                log.info('Initializing the view..');
                await tester.pumpWidget(const MyApp());
                await tester.pumpAndSettle();
                log.info('View initialized!..');
              },
            ),
            When(
              'I increment the counter',
              (tester, log, box, mocks, [example, binding]) async {
                log.info('Setting the counter to 0, finding reset button..');
                final resetButton = find.byKey(ConstKeys.resetButton);
                expect(resetButton, findsOneWidget);
                await tester.tap(resetButton);
                await tester.pumpAndSettle();
                log.success(
                    'Reset button found and tapped! Finding increment button..');
                final button =
                    find.byTooltip(ConstTooltips.incrementModelCounter);
                expect(button, findsOneWidget);
                log.success('Increment button found!');
                final nrOfDecrements = example.firstValue();
                for (int x = 0; x < nrOfDecrements; x++) {
                  log.info('Increment button tapped! ${x + 1}/$nrOfDecrements');
                  await tester.tap(button);
                  await tester.pumpAndSettle();
                }
                log.success('Counter incremented!');
              },
            ),
            Then(
              'We expect the modelCounter to have the value of the increments',
              (tester, log, box, mocks, [example, binding]) async {
                log.info('Finding value counter value..');
                final valueCounterValue = (find
                        .byKey(ConstKeys.valueOfTheModelCounter)
                        .evaluate()
                        .single
                        .widget as Text)
                    .data;
                log.success('Value counter value found! ($valueCounterValue)');
                expect(valueCounterValue, example.firstValue().toString());
              },
            ),
          ],
        );
}

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

  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Gherkin Integration Test',
      theme: ThemeData(
        primarySwatch: Colors.green,
      ),
      home: const GherkinIntegrationTestView(),
    );
  }
}

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

1 回复

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


当然,以下是一个关于如何在Flutter项目中集成并使用gherkin_integration_test插件进行行为驱动开发(BDD)的示例代码案例。这个插件允许你使用Gherkin语法编写测试,从而以一种更加人类可读的方式描述测试场景。

1. 添加依赖

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

dependencies:
  flutter:
    sdk: flutter

dev_dependencies:
  flutter_test:
    sdk: flutter
  gherkin: ^7.0.0 # 确保版本号是最新的
  gherkin_integration_test: ^2.0.0 # 确保版本号是最新的

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

2. 配置测试环境

接下来,你需要设置测试环境。创建一个新的目录来存放你的Gherkin测试文件,例如test_driver/。在这个目录下,创建一个名为features/的子目录,并在其中创建一个.feature文件,比如example.feature

test_driver/features/example.feature内容示例:

Feature: Example Feature

  Scenario: Adding two numbers
    Given I have entered 5 into the calculator
    And I have entered 3 into the calculator
    When I press add
    Then the result should be 8 on the screen

3. 实现步骤定义

现在,你需要为上述场景中的每个步骤提供实现。在test/目录下创建一个新的Dart文件,比如step_definitions.dart

test/step_definitions.dart内容示例:

import 'package:flutter_driver/flutter_driver.dart';
import 'package:gherkin/gherkin.dart';
import 'package:test/test.dart';

// 定义一个World类来存储测试状态
class CustomWorld extends World {
  FlutterDriver? driver;

  Future<void> setUp() async {
    driver = await FlutterDriver.connect();
  }

  Future<void> tearDown() async {
    if (driver != null) {
      driver!.close();
    }
  }
}

// 定义步骤
StepDefinitionGeneric GivenIHaveEnteredNumberIntoTheCalculator(int number) {
  return given<int>('I have entered {int} into the calculator', (int num) async (context) {
    // 实现输入数字的逻辑
    // 这里假设有一个TextField可以输入数字
    await context.world<CustomWorld>().driver!.tap(find.byValueKey('numberField'));
    await context.world<CustomWorld>().driver!.enterText(num.toString());
  });
}

StepDefinitionGeneric AndIHaveEnteredAnotherNumberIntoTheCalculator(int number) {
  return and<int>('I have entered {int} into the calculator', (int num) async (context) {
    // 再次输入另一个数字
    await context.world<CustomWorld>().driver!.tap(find.byValueKey('numberField'));
    await context.world<CustomWorld>().driver!.clear(); // 清除之前输入的内容
    await context.world<CustomWorld>().driver!.enterText(num.toString());
  });
}

StepDefinition WhenIPressAdd() {
  return when('I press add', (context) async {
    // 实现点击加法按钮的逻辑
    await context.world<CustomWorld>().driver!.tap(find.byValueKey('addButton'));
  });
}

StepDefinition ThenTheResultShouldBeOnTheScreen(int expectedResult) {
  return then<int>('the result should be {int} on the screen', (int result) async (context) {
    // 验证结果
    SerializableFinder resultFinder = find.byValueKey('resultField');
    String? resultText = await context.world<CustomWorld>().driver!.getText(resultFinder);
    expect(resultText, equals(result.toString()));
  });
}

void main() {
  var config = GherkinRunnerConfig()
    ..featureDirectory = Directory('test_driver/features')
    ..stepDefinitions = [
      GivenIHaveEnteredNumberIntoTheCalculator,
      AndIHaveEnteredAnotherNumberIntoTheCalculator,
      WhenIPressAdd,
      ThenTheResultShouldBeOnTheScreen,
    ]
    ..restartAppBetweenScenarios = true
    ..worldType = CustomWorld;

  test('runs gherkin tests', () async {
    await GherkinRunner.start(config);
  }, timeout: Timeout.none);
}

4. 运行测试

确保你的Flutter应用正在运行,并且Flutter Driver可以连接到它。然后,使用以下命令运行测试:

flutter drive --target=test/step_definitions.dart

这将启动Gherkin测试运行器,并按照你在.feature文件中定义的场景执行测试。

总结

通过上述步骤,你已经成功地在Flutter项目中集成了gherkin_integration_test插件,并使用Gherkin语法编写和运行了集成测试。这种方法使得测试更加可读和易于维护,同时也遵循了行为驱动开发的原则。

回到顶部