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