Categories
Dart Flutter

BDD With Gherkin In Flutter

In this post we will learn about Behavior Driven Development BDD with Gherkin in Flutter. We will introduce the concept of BDD and learn how we can write Gherkin syntax feature files and parse them in Flutter.

We will go over writing step definitions in detail and setup automated testing for the steps.

Introduction To BDD

Behavior Driven Development(BDD) is a software development process which encourages collaboration between software developers, testers and business clients. It bridges the gap between non-tech and tech team by involving everyone in the process.

BDD is just an idea about how software development process should be and not a tool in itself. The actual practice of BDD is largely facilitated by a special language called Gherkin.

Gherkin

Gherkin is a powerful language specification which supports Behavior Drive Development. It serves two purposes:

  • as your project’s documentation and
  • as your project’s automated tests documentation

The best part of Gherkin language is that it is written in almost plain human language. In fact, you don’t even have to write it in English language. You can have it written in your own language.

This enables business owners to participate in writing documentation as well as the test cases of the software or feature.

Truenary Solutions

A simple Gherkin document looks something like this:

Feature: Guess the word

  Scenario: Maker starts a game
    When the Maker starts a game
    Then the Maker waits for a Breaker to join

As you can see, it is almost plain English. Such document can be used as a product documentation as well as test case documentation.

Learn more about Gherkin

So how can we implement BDD in Flutter?

Implementing BDD With Gherkin In Flutter

We will make use of Gherkin to demonstrate BDD use in Flutter. We will do so by first creating a Gherkin document and then writing some automated tests with the help of a cool plugin called flutter_gherkin.

The plugin flutter_gherkin parses Gherkin syntax and allows to run Integration Tests in Flutter applications. For running the tests, it connects with FlutterDriverExtension under the hood.

The plugin itself is very well documented and getting started is quite easy.

Setup Gherkin In Flutter

Let’s start by creating a new Flutter project.

flutter create bdd_flutter

A boiler plate “Flutter Counter” app is created. If you run the app right now you should see this:

Flutter Counter App
Flutter Counter App

The app already has a functionality where tapping the plus button increments the value of a counter on the screen.

If we documented this feature in Gherkin syntax, it would look something like this.

Feature: Counter Button

    As a user
    I want to tap the plus button
    So that I can see the counter increment

    Scenario: User taps on counter button
        Given the user is at the counter dashboard
        And the counter value is at 0
        When the user taps on the plus button
        Then the counter value is at 1

Next we will write automated tests for this scenario.

Automated Integration Test In Flutter With Gherkin

Let’s start by creating folder that will contain all the test files. Create following folder structures in the top level of the project.

- test_driver/features
- test_driver/steps

Then add reference to the flutter_gherkin plugin.

flutter_gherkin: ^1.1.5

Now, we create an entry point for the the tests.

Add a file test-driver/app.dart:

import '../lib/main.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_driver/driver_extension.dart';

void main() {
  // This line enables the extension
  enableFlutterDriverExtension();

  // Call the `main()` function of your app or call `runApp` with any widget you
  // are interested in testing.
  runApp(MyApp());
}

Next add a feature file counter_button.feature inside the features folder.

Feature: Counter Button

    As a user
    I want to tap the plus button
    So that I can see the counter increment

    Scenario: User taps on counter button
        Given the user is at the counter dashboard
        And the counter value is at 0
        When the user taps on the plus button
        Then the counter value is at 1

Now we will implement this feature as automated tests.

Gherkin Step Implementations

In Gherkin, each scenario has multiple steps which begin with keywords like Given, When, And and Then. When writing automated tests, we implement each step.

So, for each step:

  • Arrange any needed data,
  • Act out any events and
  • Assert if output is same as expectation.

Let’s begin by implementing the first step:

Given the user is at the counter dashboard

Inside the steps folder, add a new file counter_button_steps.dart.

Each step of a scenario is made of plain words. However, it is possible to mark some words as variables. We can create even more complex arguments like lists and tables.

The flutter_gherkin has good support for parsing these Gherkin syntax.

Implementing Given Step

To implement the Given step, we extend the Given class.

import 'package:gherkin/gherkin.dart';

class UserIsInDashboardStep extends Given {
  @override
  Future<void> executeStep() async {
    print('executing UserIsInDashboardStep..');
    // implement your code
  }

  @override
  RegExp get pattern => RegExp(r"the user is at the counter dashboard");
}

The Regex syntax is required for the plugin to parse the Gherkin syntax into their appropriate step definitions.

Each step definition is a unique class.

We will implement the body part of this step later. First let’s finish setting up the test framework so that we can execute test steps.

Create FlutterTestConfiguration

Create another file test_driver/app_test.dart which will contain FlutterTestConfiguration.

import 'dart:async';
import 'package:flutter_gherkin/flutter_gherkin.dart';
import 'package:gherkin/gherkin.dart';
import 'package:glob/glob.dart';

import 'steps/counter_button_steps.dart';

Future<void> main() {
  final config = FlutterTestConfiguration()
    ..features = [Glob(r"test_driver/features/**.feature")]
    ..reporters = [
      ProgressReporter(),
      TestRunSummaryReporter(),
      JsonReporter(path: './report.json')
    ] 
    ..hooks = []
    ..stepDefinitions = []
    ..customStepParameterDefinitions = [

    ]
    ..restartAppBetweenScenarios = true
    ..targetAppPath = "test_driver/app.dart"
    ..exitAfterTestRun = true; 
  return GherkinRunner().execute(config);
}

The FlutterTestConfiguration allows many different configuration options which we will look at later.

Right now, let’s add the UserIsInDashboardStep step.

..stepDefinitions = [
      UserIsInDashboardStep()
    ]

Running Feature File With Dart

Now that we have the test framework setup, let’s see what happens when we run our tests.

dart test_driver/app_test.dart --feature="counter_button.feature"

When you run the above command from the terminal, you should see that the first Given step was run successfully but step definitions for other steps were not found. So, the output appears as below:

:~/working/StackSecrets/bdd_flutter$ dart test_driver/app_test.dart --feature="counter_button.feature"
Starting Flutter app under test 'test_driver/app.dart', this might take a few moments
[info ] FlutterDriver: Connecting to Flutter application at http://127.0.0.1:33735/3jKNZKbIsxM=/
[trace] FlutterDriver: Isolate found with number: 3671156546877879
[trace] FlutterDriver: Isolate is not paused. Assuming application is ready.
[info ] FlutterDriver: Connected to Flutter application.
Running scenario: User taps on counter button # ./test_driver/features/counter_button.feature:6
executing UserIsInDashboardStep..
   √ Given the user is at the counter dashboard # ./test_driver/features/counter_button.feature:7 took 2ms
GherkinStepNotDefinedException:       Step definition not found for text:

        'And the counter value is at 0'

      File path: ./test_driver/features/counter_button.feature#8
      Line:      And the counter value is at 0
...
..

Implement Steps In Detail

Let’s continue implementing the first Given step now.

In this step, we want to make sure the dashboard has loaded. So, how can we verify this is true?

Well, we can verify a widget has loaded in many different ways:

  • look for widget with a specific key
  • find widget by type
  • ensure certain text, tool-tip etc are loaded.

In our case, let’s assert the following label has been loaded:

You have pushed the button this many times:

Since we need to interact with Flutter widgets, we will need to have access to flutter_driver instance. So we will use the GivenWithWorld class for this step.

class UserIsInDashboardStep extends GivenWithWorld<FlutterWorld> {
  @override
  Future<void> executeStep() async {
    final locator = find.text('You have pushed the button this many times:');
    var locatorExists = await FlutterDriverUtils.isPresent(locator, world.driver);
    expectMatch(true, locatorExists);
  }

  @override
  RegExp get pattern => RegExp(r"the user is at the counter dashboard");
}

We have implemented our first step.

Implement And Step

Now, let’s move onto another step:

And the counter value is at 0

This step begins with And and has a integer at the end which can be used as argument.

So, our step definition can use the And1WithWorld class for this purpose.

Add A Key To Identify Counter Value Widget

We need to check if the counter value is initially at 0. To do this, let’s assign a key to the counter text widget in the main.dart file.

Text(
      '$_counter',
      key: Key('counter-val-key'),
      style: Theme.of(context).textTheme.display1,
   ),
Get Value Of Widget By Key

Now in the step definition, we can get the value of this widget.

class CounterValueStep extends And1WithWorld<int, FlutterWorld> {
  @override
  Future<void> executeStep(int expectedVal) async {
    final locator = find.byValueKey('counter-val-key');
    var counterVal  = await FlutterDriverUtils.getText(world.driver, locator);

    expectMatch(expectedVal, int.parse(counterVal));
  }

  @override
  RegExp get pattern => RegExp(r"the counter value is at {int}");
}

For parsing the integer value of counter we are using {int} in the RegExp.

Implement When Step Tapping On Button

Now we have reached the third step.

When the user taps on the plus button

Here we need to replicate the tap interaction on the plus button. For this we can find the button to tap by it’s tool-tip.

class UserTapsIncrementButton extends WhenWithWorld<FlutterWorld> {
  @override
  Future<void> executeStep() async {
    final locator = find.byTooltip('Increment');
    await FlutterDriverUtils.tap(world.driver, locator);
  }

  @override
  RegExp get pattern => RegExp(r"the user taps on the plus button");
}

Run All The Steps

That’s all the steps we have to write for this feature! Now make sure you have added each steps in the app_test.dart‘s stepDefintions:

..stepDefinitions = [
      UserIsInDashboardStep(),
      CounterValueStep(),
      UserTapsIncrementButton()
    ]

Run the tests.

dart test_driver/app_test.dart --feature="counter_button.feature"

And voila! All the tests are now passing.

:~/working/StackSecrets/bdd_flutter$ dart test_driver/app_test.dart --feature="counter_button.feature"
Starting Flutter app under test 'test_driver/app.dart', this might take a few moments
[info ] FlutterDriver: Connecting to Flutter application at http://127.0.0.1:43445/1k_hg6Qj3ow=/
[trace] FlutterDriver: Isolate found with number: 2970018821249871
[trace] FlutterDriver: Isolate is not paused. Assuming application is ready.
[info ] FlutterDriver: Connected to Flutter application.
Running scenario: User taps on counter button # ./test_driver/features/counter_button.feature:6
   √ Given the user is at the counter dashboard # ./test_driver/features/counter_button.feature:7 took 43ms
   √ And the counter value is at 0 # ./test_driver/features/counter_button.feature:8 took 44ms
   √ When the user taps on the plus button # ./test_driver/features/counter_button.feature:9 took 322ms
   √ Then the counter value is at 1 # ./test_driver/features/counter_button.feature:10 took 35ms
PASSED: Scenario User taps on counter button # ./test_driver/features/counter_button.feature:6
Restarting Flutter app under test
1 scenario (1 passed)
4 steps (4 passed)
0:00:04.293000
Terminating Flutter app under test

You might be thinking we never implemented the fourth step:

Then the counter value is at 1

Yet all tests passed. This is because the fourth step is same as the second step; only the int argument values are different.

In such cases, same step definition works.

So, if you are a little careful when writing scenarios, you can actually reduce a lot of step re-writes.

Wrapping Up

In this tutorial, we learnt about Behavior Driven Development (BDD) in Flutter. We wrote feature files in Gherkin syntax and implemented the step definitions for a simple Flutter app. We covered the basics of parsing and running scenarios using flutter_gherkin plugin.

What’s next?

Learn more about Gherkin use in Flutter (coming soon).

Don’t forget to check out our other articles on Flutter and Dart!

Leave a Reply

Your email address will not be published. Required fields are marked *