Flutter行为驱动开发插件gherkin_unit_test的使用

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

Flutter行为驱动开发插件gherkin_unit_test的使用

🧪 Gherkin Unit Test

此包基于称为Gherkin的行为驱动开发(BDD)语言。这种语言使我们能够以直观和易读的方式设计和执行测试。对于开发经验较少的人来说,这些测试也易于理解,因为语法非常接近英语。

大多数测试看起来像这样:

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

我们框架中的类有:

  • UnitTest
  • UnitFeature
  • UnitScenario
  • UnitExample
  • UnitStep (抽象)
    • Given
    • When
    • Then
    • And
    • But

从上到下,每个类可以包含多个下一级别的类(一对多)。一个UnitTest可以包含多个UnitFeature,后者又可以包含多个UnitScenario,以此类推。

🛠 Implementation 实现

创建测试类

开始创建继承自UnitTest类的测试类。然后创建一个不带参数的构造函数,但要调用带有description和空的features列表的父类构造函数。

class DummyUnitTest extends UnitTest {
  DummyUnitTest()
      : super(
          description: 'All unit tests regarding dummies',
          features: [],
        );
}

Features 功能

features列表中定义第一个UnitFeature。给它一个名称和一个空的scenarios列表。

UnitFeature也是定义我们要测试的systemUnderTest的地方。这个参数接受一个回调,在其中你可以执行任何逻辑来初始化systemUnderTest。这个回调会在你指定的任何setUp方法之后执行。

在这个例子中,我们将使用DummyService()作为我们的systemUnderTestDummyServicedummyMock作为参数,我们将它保存到UnitMocks对象中,以便稍后根据需要操作它。

class DummyUnitTest extends UnitTest {
  DummyUnitTest()
      : super(
          description: 'All unit tests regarding dummies',
          features: [
            UnitFeature<DummyService>(
              description: 'Saving of dummies',
              setUpMocks: (mocks) {
                mocks.write(DummyMock());
              },
              systemUnderTest: (mocks) {
                return DummyService(dummyDependency: mocks.read(DummyMock));
              },
              scenarios: [],
            ),
          ],
        );
}

Scenarios 场景

现在考虑一下你的测试中可能发生哪些场景。对于这个例子,我们将使用“成功的保存”和“失败的保存”作为可能的场景。

使用UnitScenario类创建这两个场景,并将它们放在空列表中。我们还传递一个描述和这次一个空的steps列表。

class DummyUnitTest extends UnitTest {
  DummyUnitTest()
      : super(
          description: 'All unit tests regarding dummies',
          features: [
            UnitFeature<DummyService>(
              description: 'Saving of dummies',
              setUpMocks: (mocks) {
                mocks.write(DummyMock());
              },
              systemUnderTest: (mocks) {
                return DummyService(dummyDependency: mocks.read(DummyMock));
              },
              scenarios: [
                UnitScenario(
                  description: 'Saving a good dummy should succeed',
                  steps: [],
                ),
                UnitScenario(
                  description: 'Saving a bad dummy should fail',
                  steps: [],
                ),
              ],
            ),
          ],
        );
}

Steps 步骤

现在是关键时刻。为每个场景定义步骤。我们可以访问GivenWhenThenAndBut。虽然所有这些步骤在后台基本上做同样的事情,但正确使用它们可以帮助你计划、设计和执行测试,以一种直观和正确的BDD方式。

每个步骤需要一个描述和一个回调。回调提供了以下参数:

  • SUT systemUnderTest: 我们之前在UnitScenario中指定的类,代表我们要测试的单元。
  • Log log: 允许在测试中微妙地记录步骤信息的类。
  • UnitBox box: 可用于在整个UnitScenario的一系列步骤中写入和读取值的映射。任何通过box.write(key, value)写入的值都可以在后续步骤中检索,直到被移除或所有步骤执行完毕。使用box.read(key)读取值时会自动将其转换为你指定的类型。
  • UnitMocks mocks: 在整个UnitTestUnitFeature和/或UnitScenario中持久存在的盒子,可以用来存储你需要的模拟对象,以便稍后检索并根据需要存根方法。
  • UnitExample? example: 可选的“场景大纲”示例,可以在UnitScenario中指定。

使用UnitBox的例子:

[
  Given(
    'This is an example for the UnitBox',
    (systemUnderTest, log, box, mocks, [example]) {
      box.write('isExample', true);
    },
  ),
  When(
    'we write some values',
    (systemUnderTest, log, box, mocks, [example]) {
      box.write('exampleValue', 1);
      box.write('mood', 'happy');
    },
  ),
  Then(
    'all the values should be accessible up until the last step.',
    (systemUnderTest, log, box, mocks, [example]) {
      final bool isExample = box.read('isExample');
      final int exampleValue = box.read('exampleValue');
      final String mood = box.read('mood');
      expect(isExample, true);
      expect(exampleValue, 1);
      expect(mood, 'happy');
    },
  ),
]

结合所有这些信息,我们可以最终完成并设置成功场景:

class DummyUnitTest extends UnitTest {
  DummyUnitTest()
      : super(
          description: 'All unit tests regarding dummies',
          features: [
            UnitFeature<DummyService>(
              description: 'Saving of dummies',
              setUpMocks: (mocks) {
                mocks.write(DummyMock());
              },
              systemUnderTest: (mocks) {
                return DummyService(dummyDependency: mocks.read(DummyMock));
              },
              scenarios: [
                UnitScenario(
                  description: 'Saving a good dummy should succeed',
                  steps: [
                    Given(
                      'The dummy service is initialised',
                      (systemUnderTest, log, box, mocks, [_]) {
                        mocks.read(DummyMock).stubWhatever();
                        // TODO(you): Initialise service
                      },
                    ),
                    When(
                      'We call the dummy service with dummy info',
                      (systemUnderTest, log, box, mocks, [example]) {
                        // TODO(you): Call dummy service with dummy info
                      },
                    ),
                    Then(
                      'It should succeed',
                      (systemUnderTest, log, box, mocks, [example]) {
                        // TODO(you): Verify success
                      },
                    ),
                  ],
                ),
                UnitScenario(
                  description: 'Saving a bad dummy should fail',
                  steps: [],
                ),
              ],
            ),
          ],
        );
}

Bonus UnitSteps 额外的UnitSteps

因为我们并不是每个人都想以相同的方式编写测试,所以我们也创建了这些组合步骤类,允许创建相同的单元测试,但步骤更少。

  • GivenWhenThen: 当你不想创建和使用单独的GivenWhenThen步骤功能时,这允许你在一步中编写整个测试。
  • WhenThen: 当你不想创建和使用单独的WhenThen步骤功能时,这允许你将两个步骤合并为一个。
  • Should: 当你觉得使用步骤不是你的风格时,这个步骤用一句话定义整个测试。

⚡️ Almost there! 几乎完成!

虽然这可能完全符合我们的测试需求,但我们还有一些功能可以让测试更加强大。

建筑相关的方法

每个类都有访问这些方法的功能,并且它们将以类似的方式运行:

  • setUpEach - 将在每个UnitScenario开始时运行。
  • tearDownEach - 将在每个UnitScenario结束时运行。
  • setUpOnce - 将在所选类开始时运行一次。
  • tearDownOnce - 将在所选类结束时运行一次。

使用这些方法的例子:

class DummyUnitTest extends UnitTest {
  DummyUnitTest()
      : super(
          description: 'All unit tests regarding dummies',
          setUpOnce: (mocks, systemUnderTest) async {
            await AppSetup.initialise(); // Runs once at the start of this test.
          },
          setUpEach: (mocks, systemUnderTest) async {
            systemUnderTest.reset();
          },
          tearDownOnce: (mocks, systemUnderTest) async {
            await AppSetup.dispose(); // Runs once at the end of this test.
          },
          features: [
            UnitFeature<DummyService>(
              description: 'Saving of dummies',
              setUpEach: (mocks, systemUnderTest) {
                // TODO(you): Do something
              },
              setUpMocks: (mocks) {
                mocks.write(DummyMock());
              },
              systemUnderTest: (mocks) {
                return DummyService(dummyDependency: mocks.read(DummyMock));
              },
              scenarios: [
                UnitScenario(
                  description: 'Saving a good dummy should succeed',
                  tearDownEach: (mocks, systemUnderTest) {
                    // TODO(you): Do something
                  },
                  examples: [
                    const UnitExample(values: [1]),
                    const UnitExample(values: [5]),
                    const UnitExample(values: [10]),
                  ],
                  steps: [
                    Given(
                      'I access the example value',
                      (systemUnderTest, log, box, mocks, [example]) {
                        final int exampleValue = example!.firstValue();
                      },
                    )
                  ],
                ),
                UnitScenario(
                  description: 'Saving a bad dummy should fail',
                  steps: [],
                ),
              ],
            ),
          ],
        );
}

✅ Success! 成功!

现在要运行这些测试,你只需要将DummyUnitTests添加到主测试函数中,点击运行并祈祷成功。

void main() {
  DummyUnitTests().test();
}

示例代码

下面是一个完整的示例代码,展示了如何使用gherkin_unit_test进行测试:

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:gherkin_unit_test/unit_test.dart';

import 'gherkin_unit_test_view_model.dart';

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

/// Just here to showcase the package.
///
/// Example project /test has all type of tests that fully tests the example project.
class IncrementModelCounterScenario
    extends UnitScenario<GherkinUnitTestViewModel, UnitExample> {
  IncrementModelCounterScenario()
      : super(
          description: 'Increment the modelCounter',
          systemUnderTest: (_) => GherkinUnitTestViewModelMock(),
          examples: [
            const UnitExample(values: [1]),
            const UnitExample(values: [5]),
            const UnitExample(values: [10]),
          ],
          steps: [
            Given(
              'The counter is at 0',
              (systemUnderTest, log, box, mocks, [example]) {
                systemUnderTest.reset();
                expect(systemUnderTest.modelCounter, 0);
              },
            ),
            When(
              'I increment the counter',
              (systemUnderTest, log, box, mocks, [example]) {
                final int nrOfIncrements = example.firstValue();
                log.value(nrOfIncrements, 'Number of increments');
                for (int increment = 0; increment < nrOfIncrements; increment++) {
                  systemUnderTest.incrementModelCounter();
                }
                box.write('nrOfIncrements', nrOfIncrements);
              },
            ),
            Then(
              'We expect the modelCounter to have the value of the increments',
              (systemUnderTest, log, box, mocks, [example]) {
                final int nrOfIncrements = box.read('nrOfIncrements');
                expect(systemUnderTest.modelCounter, nrOfIncrements);
                log.success();
              },
            ),
          ],
        );
}

class GherkinUnitTestViewModelMock extends GherkinUnitTestViewModel {
  @override
  void rebuild() {}
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Gherkin Unit Test',
      theme: ThemeData(
        primarySwatch: Colors.green,
      ),
      home: const GherkinUnitTestView(),
    );
  }
}

希望这些内容能帮助你更好地理解和使用gherkin_unit_test插件!如果有任何问题或需要进一步的帮助,请随时提问。


更多关于Flutter行为驱动开发插件gherkin_unit_test的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter行为驱动开发插件gherkin_unit_test的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,以下是如何在Flutter项目中使用gherkin_unit_test插件进行行为驱动开发(BDD)的示例代码。gherkin_unit_test插件允许你使用Gherkin语法编写测试用例,这对于BDD来说是非常有用的。

1. 添加依赖

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

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

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

2. 编写Gherkin特性文件

test/features目录下创建一个新的.feature文件,例如example.feature

Feature: 简单的数学运算
  Scenario: 加法运算
    Given 我有一个计算器
    When 我输入 2 + 2
    Then 结果应该是 4

  Scenario: 减法运算
    Given 我有一个计算器
    When 我输入 5 - 2
    Then 结果应该是 3

3. 编写步骤定义

接下来,在test目录下创建一个新的Dart文件,例如step_definitions.dart,并在其中定义与Gherkin步骤对应的测试逻辑:

import 'package:flutter_test/flutter_test.dart';
import 'package:gherkin_unit_test/gherkin_unit_test.dart';

// 简单的计算器类
class Calculator {
  int add(int a, int b) => a + b;
  int subtract(int a, int b) => a - b;
}

void main() {
  var feature = loadFeature('test/features/example.feature');

  feature.scenario('加法运算', () {
    var calculator = Calculator();
    var result;

    given('我有一个计算器', () {});

    when('我输入 2 + 2', () {
      result = calculator.add(2, 2);
    });

    then('结果应该是 4', () {
      expect(result, 4);
    });
  });

  feature.scenario('减法运算', () {
    var calculator = Calculator();
    var result;

    given('我有一个计算器', () {});

    when('我输入 5 - 2', () {
      result = calculator.subtract(5, 2);
    });

    then('结果应该是 3', () {
      expect(result, 3);
    });
  });
}

4. 运行测试

你可以使用Flutter的测试运行器来运行这些测试。在命令行中运行:

flutter test test/step_definitions.dart

注意事项

  1. 路径:确保.feature文件和Dart文件的路径正确。
  2. 依赖版本:在实际使用中,请确保gherkin_unit_test的版本与Flutter项目兼容。
  3. 调试:如果测试失败,检查Gherkin步骤与Dart代码之间的映射是否正确。

通过以上步骤,你就可以在Flutter项目中使用gherkin_unit_test插件进行行为驱动开发了。希望这个示例能帮助你快速上手。

回到顶部