Alo
Alo

Alo

An introduction to Double Loop - BDD + TDD

Photo by Baptiste on Unsplash

An introduction to Double Loop - BDD + TDD

A powerful workflow to keep your client happy while going kitesurfing

Alo's photo
Alo
Β·Mar 28, 2022Β·

15 min read

In this series of articles, we will explore a very powerful development technique: Double Loop TDD.

In this first article, we will explore the outer loop, BDD: Behaviour Driven Development

But for now, forget about Double Loop for a second, and let's focus on BDD

"I want to go kitesurfing" or how to avoid miscommunication πŸ‘‚

image.png

As a freelancer, I had more than once spent time writing features that weren't required or misinterpreted.

It is time ill-spent that could have been used to develop a most urgent feature - or used to go out kitesurfing πŸͺ

Not great for our customers, not great for us.

I thought: How can I avoid that?

It seems that the problem is nothing but a communication issue.

And Behaviour Driven Development, I discovered. Amen. πŸ™

Our clients are from various industries. They have their jargon and technical knowledge. We, as developers, have our jargon and technical knowledge.

BDD is all about bridging the gap between us, developers, and them, industry professionals.

If we were to talk about a banker about whether we should use Event Sourcing or not for the persistence layer, (s)he would probably have a hard time understanding us.

But it's our role, as freelancers and developers, to make sure we translate our client's needs into technical implementation. At the end of the day, we are here to add value to their business.

How can we write software requirements that non-technical people can read?

How do we know that our code is fulfilling the needs of our users if we're not using the same language?

Wouldn't it be great if we could use concrete, precise and solid cases to illustrate the needs of the software?

It is the problem Behaviour Driven Development is addressing.

BDD to the rescue

As stated before, BDD is the bridge between our clients or 'domain experts', and us, developers.

image.png

It is a method that allows us to develop together with the system requirements with a language that both parties can understand and agree upon.

Behaviour Driven Development is a development technique where we define our technical requirement by defining the behaviour expected, in plain English.

For example, let's put ourselves in the shoes of a customer. Let's forget about coding for a moment - we are customers, and we have a need. We talk a professional about solving our problem.

What do we need? Well, we read a lot, but we live in a small place. We need somewhere to put our books on the wall, a shelf.

If we commission someone to solve our issue, i.e. build us a shelf, we expect it to be able to hold at least ten books. That's the behaviour expected.

We don't say, I want the shelf to:

  • be straight with a tolerance of 2mm per metre,
  • have a resistance of 10 newton-meters on shear forces,
  • be fixed with hollow walls anchors driven using a power drill operated at 350 rpm,
  • be built with pine timber store in a warehouse with a humidity of no more than 65% on average,
  • be 800mm long by 500mm deep.

No, what we want is a shelve, that can hold 10 books.

If the handy(wo)man comes and tells you: "I'm going to drive the anchor in the wall with my drill at a speed of 350 rpm, is that OK for you?"

You'd be like "Meh... Yeah. I guess so? I don't know?"

But as software developers, we do it all the time!

No, we say: "Whatever, you are the professional. As long as it is done properly and can hold 10 books, I'm happy with that."

That's the behaviour required.

It is the same for software: our client has a need, we provide a solution.

We cannot expect our client to have a knowledge of the underlying technical aspects of the solution.

But we need a common ground to describe the expected result: the behaviour.

BDD is a way to work around that common ground. We start from the behaviour and we build the software that meets the need of the client.

On top of that, it has some incredible side effects:

  • Your code is being tested as result, and thus protected against bugs and regressions,
  • You are documenting your code as you go,
  • For freelancers, it's a great way to 'scope' your work.

We save time, the client is happy and we go kitesurfing. Sweet! Where do I sign?

I'm sold: How do you BDD?

The image below describes the different steps of BDD.

We start from a User Story and go through the three steps:

  • πŸ”¬ Discovery,
  • πŸ‘¨β€πŸ”¬ Formulation,
  • πŸ€– Automation.

The result is our Working Software πŸŽ‰

image.png

What I like about the image above is that it captures the 'feedback loop', which are the greens arrows on the left-hand side.

At each step, you might want to revisit the previous one to clarify a previous statement.

We will discuss more it later in this post, but keep in mind that BDD is not a science, it's a process.

It requires discipline, but the benefits you rip from it are incredible.

Discovery πŸ”¬

The hardest single part of building a software system is deciding precisely what to build. – Fred Brooks, The mythical man-month

The Discovery step is about discovering and defining the software's expected behaviour.

To do so, you need to talk to the Domain experts (i.e. the persons for whom you are building the application).

Often, it is simply your client. But sometimes, you might need to reach out to another actor, such as the end-user.

If you were to build a banking app, it could be the banker itself or a customer of the bank, depending on the needs.

A simple conversation can be enough to identify the expected behaviour, but you can also organise discovery workshops.

This allows you to highlight and fill any gaps in your understanding of the business.

On top of that, it is a great tool to identify and prioritise features to be developed.

image.png

At this stage, we are not writing any code or tests.

We are simply documenting our conversation and identifying our client needs before moving on to the formulation step.

You probably already practise this when you talk to your client! Don't overthink it.

There are no hard rules about the Discovery Step, we just want to understand and document our client's needs through a discussion.

Formulation πŸ‘¨β€πŸ”¬

image.png The idea of the formulation is to choose a requirement identified in the Discovery step and formulate it into a language that can be both understood by the computer and the human.

Why would we want to do that?

There are two reasons:

  • we can get feedback from the domain experts if we didn't formulate the requirement properly,
  • we can automate these requirements.

There are several options available, but the most common syntax used is Gherkin.

Don't worry Gherkin has a very simple API with half a dozen keywords.

Gherkin allows you to write behaviour requirements in plain English that can be then transformed and run by a test runner.

Now, we can move to the automation step where we turn our formulated requirement into automated tests.

Automation πŸ€–

In the automation step, we take our newly written behaviour and turn it into a test.

image.png

One of the popular options is Cucumber.

You might be like: "Ouch! Gherkin, Cucumber... How many new tools do I need to learn?"

Bear with me, it's a lot more than it sounds simple.

Both of these tools are very simple syntax. There's nothing fancy here.

We write our requirements in plain English using a syntax called Gherkin during the Formulation step.

These requirements are translated into automated tests in the Automation step by Cucumber.

That's where the strength of BDD reside.

These tests guide our development, similar to a TDD method - we follow the red-green-refactor pattern.

Summary πŸ“Ž

Don't overthink it. BDD is only composed of three steps:

  1. We discover our client needs,
  2. We formulate these features in plain English
  3. We transform these features in automated testing

image.png

The benefits:

  1. At any time, our client can review the feature: they're written in plain English! No more time was spent developing the wrong features.
  2. On our side, we now have clear boundaries and guides to build our software: the tests. These tests prevent regression and document our code.

We get the best of both worlds. BDD is a win-win for you and your client.

On top of that, as a freelancer, you now have a clear scope of work agreed upon by your client.

Demonstration time: a real example - Rebu

Imagine we are building an application for taxi drivers called Rebu.

The idea is pretty simple, the customer has an app from which (s)he can book a taxi. The application should allow taxi drivers to see customers that have requested a taxi.

This will forgo the need for an overhead office: think Uber but for licensed taxi drivers.

Now, because we now understand the power of BDD, we decide to give it a crack on this project.

We have written our User Stories and are ready to move to the next phase.

Discovery

We meet with our taxi driver, ready to dive into the discovery stage.

In practice, these conversations can be a lot more informal (or a lot more formal!), but the idea remains the same, discover the actual requirements.

Let's take the following User Story:

"As a driver, I want to be able to see potential customers looking for a ride in my area."

// Greetings and small talk between you and the taxi driver

  • "When you say in your area, how far are you thinking?"
  • "Well, probably about 5km. But if there's is none in the area, it would be great to be able to see other customers further away."
  • "So, when they are customers within 5km, you want to see them. But If there aren't you want to see customers further away? How far are you willing to drive if there are no customers around?"
  • "Well, it depends on the conditions. But in my experience, I would be willing to drive half an hour."
  • "Ok, so it's more about time to the customer than distance."
  • "Exactly."
  • "So the customers within 5km, would you put a time limit on it?"
  • "Yes, I would say 5 minutes."
  • "Alright, so what we need is a way to see customers that are less than 5min drive from your current position. If there are none, you'd like to see those within 30min."
  • "Exactly!"

// Beer time, you start chatting about more interesting stuff with the taxi driver

Great, now we've got more accurate requirements. From these, we can express our requirements.

As I've said before, you probably already do this! And that's the hardest step, so it doesn't take much more effort for most of us to rip the benefits of BDD.

Formulation

This is where we use Gherkin to write our requirements.

We turn these requirements into scenarios with precise cases.

*Feature*: As a taxi driver, I want to see potential customers in my area

*Scenario*: Potential customers in my area are looking for a ride, some less than 5 min away from me

*Given* there are three existing potential customers, Juliette 10min away, John 2min away and Bernard 5min away
*When* I look at my list of potential next customers
*Then* I should be able to see John and Juliette

*Scenario*: Potential customers in my area are looking for a ride, none less than 5 min away from me

*Given* there are three existing potential customers, Roger 15min away, Georges 32min away and Louis 11min away
*When* I look at my list of potential next customers
*Then* I should be able to see Roger and Georges

Pretty easy right? Any non-technical person can read these requirements.

Now our taxi driver can let you know if the requirements are correct or not.

If not, we go back to the discovery stage and clarify the requirements.

Otherwise, we move to the automation phase.

Note: Gherkin has a few more features, including a way to present your data into a table. I recommend you to visit the documentation

Automation

Using Cucumber, we can translate the formulated Gherkin features into tests.

Let's focus on the first scenario: Potential customers in my area are looking for a ride, some less than 5 min away from me

// Don't freak out, these are just Cucumber's way to turn our Gherkin into automated tests

const { Given, When, Then } = require("@cucumber/cucumber-js");  

Given(/^there are three existing potential customers, Juliette 10min away, John 2min away and Bernard 5min away$/, function() {  

});  

When(/^I look at my list of potential next customers$/, function() {  

});  

Then(/^I should be able to see John and Juliette$/, function() {  

});

We run the tests and... they fail. Great, let's implement the minimum amount of code required to pass the tests.

Here, are you can see, we are being guided (or driven) by our formulation.

Note: I am using a layered architecture 'Clean Architecture' type. I will dedicate a whole blog post to it. But for now, simply imagine that I am using OOP coding style and SOLID principle (such as Dependency Injections).

const { Given, When, Then } = require("@cucumber/cucumber-js");  

// We use `this` to pass data between steps, Cucumber refers to this as `World`.
// Think of it as a global variable.

Given(/^there are three existing potential customers, Juliette 10min away, John 2min away and Bernard 5min away$/, function() {  
    // We use an inMemoryRepository so we do not need to spin a DB.
    // In production, we would probably store them in DB or something similar.
    this.inMemoryRepository = new InMemoryRepository();

    // We create our three customers
    this.juliette = customerRequestTaxiUseCase({
        customer: new Customer({
            name: "Juliette", 
            adress: "Somewhere 10min away"
         }), 
        repository: this.inMemoryRepository
    });

    this.john = customeRequestTaxiUseCase({
        customer: new Customer({
           name: "John", 
           adress: "Somewhere 2 minutes away"
        }), 
        repository: this.inMemoryRepository
    });

    this.bernard = customeRequestTaxiUseCase({
        customer: new Customer({
            name: "Bernard", 
            adress: "Somewhere 5min away"
        }), 
        repository: this.inMemoryRepository
    });
});  

When(/^I look at my list of potential next customers$/, function() {  
    // Our taxi driver entity
    const driver = new TaxiDriver();
    // 'Use case' is a Clean Architecture related term.
    this.listTaxiRequest = taxiGetUnfufilledCustomerRequestsUseCase({
        taxi: driver, 
        repository: this.inMemoryRepository
     });
});  

Then(/^I should be able to see John and Bernard$/, function() {  
     expect(this.listTaxiRequest).to.not.contains(this.juliette)
     expect(this.listTaxiRequest).to.contains(this.john)
     expect(this.listTaxiRequest).to.contains(.this.bernard)
});

As you can notice, Given When and Then can be assimilated to a Arrange - Act- Assert testing pattern.

Feedback loop

image.png

Remember the green arrows on that image? That's what I refer to as the feedback loop.

At any time, we might want or need to go back a step to clarify a step.

By writing these requirements above, we realise that we don't have a way to express being 2, 5 or 10 min away from the taxi driver.

We need to go back and review our formulation: we need a real address to calculate how far the customers are from the driver in minutes.

But it raises another question:

How is the taxi driver calculating how long it would take to get to a customer position? What if there are traffic?

We return to the Discovery phase and raise the question to the taxi driver.

"These days we simply use Google maps."

Great, that's the information we needed. Back to the formulation.

We arbitrary place our taxi driver in the middle of a beautiful place in France, Doussard. "Route du Pont Monnet, 74210 Doussard, France"

We identify three other location to suit our needs:

  • "Bob Pizza, 455 Rte de la Vieille Γ‰glise, 74210 Doussard, France", 4 min away.
  • "Pizza Sympa, 44 Rue de la Poste, 74210 Doussard, France", 1 min away
  • "Domino's Pizza Annecy - Centre, 24 Av. de ChambΓ©ry, 74000 Annecy, France", 25 min away
*Feature*: As a taxi driver located at Route du Pont Monnet, 74210 Doussard, France, I want to see potential customers in my area

*Scenario*: Potential customers in my area are looking for a ride, some less than 5 min away from me

*Given* there are three existing potential customers, Juliette located at Bob pizza, John located at Pizza Sympa and Bernard, at Domino's pizza
*When* I look at my list of potential next customers
*Then* I should be able to see John and Juliette

#- "Bob Pizza, 455 Rte de la Vieille Γ‰glise, 74210 Doussard, France", 4 min away.
#- "Pizza Sympa, 44 Rue de la Poste, 74210 Doussard, France", 1 min away
#- "Domino's Pizza Annecy - Centre, 24 Av. de ChambΓ©ry, 74000 Annecy, France",  25 min away
const { Given, When, Then } = require("cucumber");  

Given(  
 /^there are three existing potential customers, Juliette located at Bob pizza, John located at Pizza Sympa and Bernard, at Domino's pizza$/,  
 function () {
    // Using an inMemoryRepository so we don't need a DB
    this.inMemoryRepository = new InMemoryRepository();
    // We will need to query Google map to get the driving distance
    this.googleMapService = new GoogleMapService();

    // Our customer now have concrete address attached to them
    this.juliette = customerRequestTaxiUseCase({
        customer: new Customer({
            name: "Juliette", 
            adress: "455 Route de la Veille Eglise, 74210 Doussard, France"
        }), 
        repository: this.inMemoryRepository
    });

    this.john = customeRequestTaxiUseCase({
        customer: new Customer({
             name: "John", 
            adress: "44 Rue de la Poste, 74210 Doussard, France"
        }), 
        repository: this.inMemoryRepository
    });

    this.bernard = customeRequestTaxiUseCase({
         customer: new Customer({
             name: "Bernard", 
            adress: "Centre, 23 Av. de Chambery, 7400 Annecy, France"
         }), 
        repository: this.inMemoryRepository
    });

 }  
);  

When(/^I look at my list of potential next customers$/, function() {  
    const driver = new TaxiDriver();
    // Notice how we use Dependency Injection to inject the map service. 
    // Today we use Google map, but tomorrow we might change to something else?
    this.listTaxiRequest = taxiGetUnfufilledCustomerRequestsUseCase({
        taxi: driver, 
        repository: this.inMemoryRepository, 
        mapService: this.googleMapService
     });
});  

Then(/^I should be able to see John and Bernard$/, function() {  
     expect(this.listTaxiRequest).to.not.contains(this.juliette)
     expect(this.listTaxiRequest).to.contains(this.john)
     expect(this.listTaxiRequest).to.contains(.this.bernard)
});

Our test fails. What's next?

We have seen a bird's eye view of BDD, but we haven't written any code to make our tests pass yet.

Read the next part where we zoom in and use the inner loop: TDD: Test-Driven Development.

Don't worry, there will be a lot more coding going on πŸ˜„

Conclusion

BDD is a must-have tool that has many benefits.

Today, you're probably already doing more than half of the work since you're probably doing the Discovery step.

When assigned a task, or when you talk to your client, you are gathering information about the features.

So why not add a small effort and rip all the benefits of BDD?

  1. It allows you to discover, understand and prioritise clients' requirements.
  2. It guides you in what you need to code, no more, no less.
  3. It prevents regression by testing your code.
  4. It documents your code as you go.
  5. As a freelancer, it defines your scope of work.

Plus, if you combine it to TDD you obtain the powerful Double Loop TDD workflow.

As you are about to discover, the Double Loop TDD allows you to write fast, efficient, bug-free code with a minimum of discipline.

And then, well, we can go kitesurfing and keep our client happy 🍹

Β 
Share this