How to make a behavior-driven development in Flutter

Flutter Bdd

A complete journey across the technique that facilitates collaboration between technical and product teams.

By Ariel Zeballes and Alan Rosas, Software Engineers at etermax

In this article, continuing with our series related to Flutter, we will develop a very simple example using behavior-driven development concepts. As its name suggests, this technique is all about the desired behavior of our application. Basically, it focuses on what we want to obtain.

We will take the default application created by Flutter and make some small changes ā€” allowing us to slightly redesign ā€” to show the main characteristics of the technique.

To avoid stretching the process, we will only have one iteration cycle (just one scenario) simple and complete enough to allow us to go over the technique from start to finish.

The full code in the example can be obtained from this repository, where the final solution is in the main branch, and the step-# branches contain the step-by-step code used in each iteration. Additionally, the source code files in their corresponding branches are linked in each of the following sections.

The tools we will use are illustrated in the word collage below.

Our first scenario

Without further due, letā€™s create the counter feature with our applicationā€™s initial scenario (first iteration) features/counter.feature

Line by line, this scenario indicates that the counter value is 10. We will show this value after restarting the application.

We must restart the app after assigning the value (10) to the counter, because, as weā€™ll notice, the app starts before running the scenario.

Now that we specified the first (and only ?) iteration, letā€™s generate the associated integration test that will guide us during the implementation.

Creating the application

Letā€™s start by creating an Android app in Flutter.

flutter create - template=app - platforms android - project-name \
flutter_bdd - org com.example .

As our BDD framework, weā€™ll use the flutter_gherkin package.

Letā€™s add it to the project and create the test_driver folder:

#Add the flutter_gherkin package
flutter pub add flutter_gherkin --dev
#Create the test_driver folder
mkdir test_driver
#Delete the example widget test
rm test/widget_test.dart

We developed an application for acceptance testing that enables the automation driver and will also run the application weā€™re developing: test_driver/app.dart

We can run the test application on an Android emulator by following these steps:

#Obtain the list of available emulators
emulator -list-avds
#Run an emulator. For example, Pixel_4_API_30
emulator @Pixel_4_API_30 &
#Run the app on the emulator
flutter run test_driver/app.dart

Our next step is creating the entry point to the configuration of our acceptance tests by means of this file: test_driver/app_test.dar

Now we can run our acceptance tests on the emulator

#run the emulator
emulator -avd Pixel_4_API_30 &
#run the acceptance test
dart test_driver/app_test.dart

The test should fail given that we didnā€™t implement the steps related to the feature.

ā€¦
Step definition not found for text:
'Given counter value is {10}'
ā€¦

As seen above, we got a suggestion on how we should create and configure the steps during the execution output.

? Implementation of the test steps

Given counter value is {10}

Now, weā€™ll implement the first step of our scenario which should assign an initial value to the counter service.

To achieve this, we have to add the file test_driver/steps/given_counter_value_is.dart.

We also have to add the step to the list of step definitions in test_driver/app_test.dart.

If we run our acceptance test again, weā€™ll notice that now weā€™re asked to implement the following step:

Step definition not found for text:

'Then I see the value 10'

Then I see the value 10

Similarly, we have to add the step ā€œThen I see the value 10ā€ to test_driver/steps/then_i_see_the_value.dart.

Again, we must add it to the list of personalized steps in test_driver/app_test.dart.

Now, when running the test weā€™ll notice that it fails for the right reason, as the application is not showing the desired initial value.

...
Step 'Then I see the value 10' did not pass, all remaining steps will be skipped #
...

Itā€™s worth mentioning that we didnā€™t assign the initial value to our counter service at any time. Weā€™ll come back to this since we need to go over some details about how to achieve this before doing it.

Improvements in the code

To make the implementation simpler, we separated the applicationā€™s three main components into three files:

Weā€™ll also delete the default logic associated with the view. lib/my_home_page.dart

Implementation

The BDD cycle consists of identifying an application use scenario and automating the test for that scenario to fail. The next step is making all the necessary changes to the application to run the test making use of TDD (repeated cycles of red-green-blue).

The cycle comes to an end when the acceptance test passes, and it is then when we can approach our next scenario.

Having identified that we need to obtain an initial counter value from an external service, letā€™s dive into the internal TDD cycle and go over the construction of each of the components of the application.

Weā€™ll follow the MVVM pattern for the communication between the views and the model.

The view

Starting with the internal TDD cycle, letā€™s create the tests associated with the view of our application.

Letā€™s add the following packages as development dependencies to test the interactions between our view and their viewmodel.

flutter pub add mockito --dev
flutter pub add build_runner --dev

Weā€™re going to use the build_runner package to generate our test mocks. To carry this out, we can leave the following process running, which will spot files including the tag @GenerateNiceMocks and generate the needed code.

flutter pub run build_runner watch --delete-conflicting-outputs &

Now, letā€™s add the view test test/my_home_page_test.dart

Letā€™s modify the view so that it uses its viewmodel lib/my_home_page.dart

And letā€™s add the viewmodel lib/my_home_page_view_model.dart

Lastly, letā€™s create the viewmodel from the main component lib/my_app.dart.

To run the view tests we can do the following:

flutter test test/my_home_page_test.dart
ViewModel

Weā€™ll need to simulate asynchronous interactions to test our viewmodel, so letā€™s add the fake_async package. This will allow us to test the interactions between the component and the counter service.

flutter pub add fake_async - dev

We must keep in mind that the tag @GenerateNiceMocks needs build_runner ā€” mentioned previously ā€” to be running.

Now, we have to add the tests that specify the desired behavior of our viewmodel and its interaction with the counter service: test/my_home_page_view_model_test.dart

In short, our viewmodel will initially show the value 0 for the counter. The view may request the current value by means of the initialize method.

When the value is obtained, the user interface has to be updated.

The CounterService interface is lib/counter_service.dart.

The implementation of our view model looks like this: lib/my_home_page_view_model.dart

#Validate our view model by running its tests
flutter test test/my_home_page_view_model_test.dart

Weā€™ll notice that there are compilation errors since the concrete implementation of the service is still missing.

Counter service

Weā€™ll suppose our counter service is external and accessible via an API REST. Now, letā€™s implement our HTTP version of the service:

First, we have to add a configuration service that allows us to obtain the base address of our service from an environment variable.

lib/configuration_service.dart

#Add the http package
flutter pub add http

Then, we have to create the test with the service specification test/http_counter_service_test.dart and implement it. lib/http_counter_service.dart

Now that we have a concrete implementation of the counter service, we can correct the compilation error in my_app.dart: lib/my_app.dart

#Run the tests of our service
flutter test test/http_counter_service_test.dart
Back to ā€˜Given counter value is {10}ā€™

Now that we know the technical details regarding the service counter, letā€™s set an initial value starting from the step of the test. To do this, weā€™ll simulate the service using wiremock. We have to run a Docker container with a service instance

docker run - rm -d -p 8080:8080 - name wiremock wiremock/wiremock

and add a simulated response in the corresponding step: lib/test_driver/steps/given_counter_value_is.dart

Then, we have to configure the environment variable in the application weā€™re going to test: lib/test_driver/app_test.dart

Now we can run our acceptance test the following way:

#Use current local ip address
dart - define=COUNTER_BASE_URL\=http://192.168.100.29:8080 test_driver/app_test.dart

Weā€™ll notice that the test runs correctly, so we can deem our first iteration in the BDD cycle completed ?.

Final remarks

As discussed in the previous sections, the BDD technique focuses on the objective behavior we want to obtain from our application (the what), leaving technical details (or the how) for the internal development cycles. Generally, it facilitates communication between the engineering and product teams by using a common language (scenarios). Lastly, itā€™s an excellent indicator of the development status, as it allows us to obtain functioning software quickly, reduce the initial level of uncertainty and discover or modify uncontemplated scenarios.

, ,