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.