Behavior-Driven Development (BDD) in iOS using Swift— Part 1
Motivation
I've been an iOS developer since 2015 and lately I've been researching a lot about how to develop code which is reliable and better aligned with my stakeholders' expectations.
Last year, I discovered Behavior-Driven Development (BDD), a software engineering methodology proposed by Dan North, and began to study it more deeply.
What I found out is that BDD is a great way to avoid re-work. By ensuring our code is both correct (i.e. it follows software engineering best practices) and right (i.e. it aligns with our customers expectations), it allows us to focus on the important (and fun) parts of the development process.
After experiencing some of the benefits and challenges of BDD, I've created a personal workflow for developing Apps in Xcode using it. This post will detail this workflow and provide some considerations about it. I hope you enjoy it.
Pre-requisites
The following guide assumes you are familiarized with Apple's Development Ecosystem. In addition to this, we assume you are using Agile software development practices.
Besides Xcode, all you need are two testing frameworks, Quick & Nimble.
Example App
The App we will be developing to better experience the workflow is a simple Weather-App. The complete code is available here.
This app uses the OpenWeather API to display weather forecast of cities to world travellers.
If you want to code together, simply create a new project and make sure you are using SwiftUI and have the "Include Unit Tests" and "Include UI Tests" checkboxes enabled.
Workflow
The workflow proposed is divided in the following steps.
- 🛠 Augmenting user stories using BDD scenarios;
- 🛠 Coding high-level features using executable specifications;
- ⏭ Coding low-level features using low-level specifications ;
- ⏭ Coding high-level UI features using executable specifications;
Items marked with 🛠 will be covered in this part, while items marked with ⏭ will be implemented in other parts.
The beauty of each one of these steps is that they are independent, but work together pretty nicely.
As such, the two steps for this post are further explained below.
Augmenting user stories using BDD scenarios
User stories are a common format used to specify what needs to be developed in an software project. These stories are lightweight requirements and are usually created in collaboration with stakeholders. Even though BDD provides collaboration oportunities for the initial phases, we will assume that the stories have been created and focus in the processes used to implement it.
In this context, user stories are usually structured in the following format.
AS A <STAKEHOLDER>
I WANT TO BE ABLE TO <CAPABILITY>
SO THAT I <VALUE>
For our example App, we will implement the user story below.
AS A WORLD TRAVELLER
I WANT TO BE ABLE TO SEE CITIES AND THEIR FORECASTS
SO THAT I CAN KNOW THE FORECAST AROUND THE WORLD
This format clearly presents the "who", the "what" and the "why" for a specific requirement. However, few direction is given to the "how" and it is part of the work of the software engineering team to figure this out.
BDD proposes user stories to be augmented by "scenarios". Scenarios are concrete examples for a user story. These examples should emerge from the collaboration among stakeholders in a project and can be discussed in simple conversations. As such, BDD enhances the collaboration capabilities of an agile team.
BDD scenarios are usually specified following a Gherkin template.
GIVEN <PRECONDITION>
WHEN <ACTION>
THEN <OUTPUT>
The "GIVEN" statement sets what is assumed to have already happened, providing a known previous state. Next, the "WHEN" statement provides the action which should alter this previous state. Finally, the "THEN" statement asserts that the modifications to the previous state have generated a new valid state.
Optionally, the GIVEN and THEN steps can have more than just one statement. These additional statements should be added below their main context using the word AND.
A user story can have one or many associated scenarios. The terms used for creating scenarios should be familiar to to all stakeholders, avoiding technical jargons.
In this post, we will take a look at the implementation of one scenario for the example user story. The complete set of developed scenarios will be available here.
Coding high-level features using executable specifications
The scenario we will be developing in this first part is the following.
Scenario — Loading forecast successfully for two cities
GIVEN the target cities are San Francisco (SFO) and Porto Alegre (POA)
AND in SFO, it is 20º, “Sunny”, with a min-max of 15º and 25º
AND in POA, it is 15º, “Cloudy”, with a min-max of 10º and 20º
AND the App has started to load the forecast for the target cities
WHEN loading finishes successfully
THEN there should be two cities loaded, SFO and POA
AND the cities should be in alphabetic order
AND it should be 15º, “Cloudy”, with a min-max of 10º and 20º
AND it should be 20º, “Sunny”, with a min-max of 15º and 25º
Our first step is to translate this scenario into an executable specification. This is where Quick & Nimble will be used. We will use Quick to exactly translate the natural-language scenario to our code "driver" and then use Nimble to assert our code outputs are the expected ones.
In your test target, create a new empty QuickSpec. A nice way to keep a reference to the feature you are implementing is to name the spec using the name of the feature and use the "describe" function provided by Quick. The final file should look similar to the one below.
From this, inside our "describe", we can use the "context" and "it" functions to translate our scenario to an executable specification. The final result should be similar to the one below.
Next, we should proceed to implementation. BDD is usually implemented using an outside-in approach, meaning we should start by writing how the code should be used prior to actually implementing it. Another way . This is aproach is also applied in Test-Driven Development (TDD) and follows a three-step process:
- Write a failing test.
- Make the test pass.
- Refactor.
Step 1 — Writing a failing test
We start by the "GIVEN" statement. One possible implementation, inspired by Clean architecture and using Combine, is presented below.
This code creates the proposed preconditions, using Mocks where appropriate. Note that Xcode has already started to warn us that none of the code we are using actually exists.
Let us withstand the burden for now and move on. Next, we implement the "WHEN" statement. This part is usually the shortest one. As such, a possible implementation using combine is presented below.
Finally, we implement the "THEN" statement. A proposal implementation is shown below.
Now that we have the "blueprint" for our code, we should proceed to actually implementing it.
Step 2— Making the test pass
Our goal now is to write the simplest code that makes the tests we have just written pass.
We will start by the code in the "GIVEN" statement, specifically the Forecast struct.
We make it Equatable so we can compare it later in the "THEN" statement. Next, the MockForecastProvider.
You wil notice that we are conforming to a ForecastProvider protocol. We are using this protocol, which wil be created later, to use dependency injection, a concern separation technique. As this provider is purely used for creating mock objects in our tests, we will keep it in the test target.
For the ForecastLoadingInteractor, we create the ForecastProvider protocol and the interactor struct which uses it.
Proceeding to the AppState, we must create a status property, which can be injected, and the forecasts property, which is used in the "THEN" step.
To end the "GIVEN" statement implementation, we simply add the perform(action: in:) method to the interactor.
Here we are using Combine's built-in Fail publisher to simulate an empty implementation.
Next, we begin implementing the "WHEN" statement. All it requires is that we actually perform the action in the interactor using the provider. We can do this by making our interactor use its provider to get the forecasts.
This update requires that we change all implementations of the ForecastProviderProtocol, such as in our MockForecastProvider.
Next, we move on to the "THEN" statement implementation. Luckily for us, we have nothing to implement here, since the properties required for assertions have been created previously.
Since our code compiles, it is time to see if our test passes.
The last assertion tells us that we have not sorted the results properly, following an alphabetic order. As such, we add this rule in our interactor.
Done. Now the test runs sucessfully.
Step 3— Refactor
Now that our test passes, we can spend some time refactoring our implementation. It is nice to remove any shortcut you might have taken or improve your abstraction at this point. Just avoid falling into the over-engineering temptation.
In our example App, we chose to keep implementation as it is.
Conclusion
This post covered the first two parts of the proposed BDD workflow.
The first part of the workflow has two main goals. First, increasing your overall understanding of the user story by collaborating with stakeholders and second, creating a high-level acceptance criteria for your user stories, i.e. the scenarios.
The second part of the workflow has one main goal, to ensure we implement code for our business rules and nothing more!
As such, our current status is:
- ✅ Augmenting user stories using BDD scenarios;
- ✅ Coding high-level features using executable specifications;
- ⏭ Coding low-level features using low-level specifications ;
- ⏭ Coding high-level UI features using executable specifications;
The next parts of the workflow will be covered in upcoming posts. In the meantime, you access the developed code here.
Finally, I hope you find this guide useful and if you want to continue learning more about BDD, sign up for a complete BDD course in iOS.
Thanks.
(EDIT: Part 2 is here)