• In this article 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.

    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.

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

    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:

                                   

    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 GivenWhenAnd 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.

0 Years in
Operation
0 Loyal
Clients
0 Successful
Projects

Words from our clients

 

Tell Us About Your Project

We’ve done lot’s of work, Let’s Check some from here