Mohamed Alnashmi

Home

Behavioral Driven Development

March 23rd, 2024

Table of Contents

Summary

  1. Create your feature file
  2. Write your Scenario
  3. Use godog to autogenerate the function definitions
  4. Implement the function definitions - bridging English and Golang
  5. Run the tests

Prerequsities

Quick Summary

BDD allows techies and business to speak in some form of a common language. Requirements are alot more clear for the developers making the acceptance criteria and sign off just easier.

Cucumber & Gherkin allows business to write their requirements in english (with a set of keywords), and the techies then convert those into steps. These scenarios are then executed as tests and validations to ensure what was requested was actually delivered.

Setting up the project

Open up the project from the first tutorial, and lets start by installing godog.

go get "github.com/cucumber/godog"  

Since our project is a small one, we'll just add the scenarios in a file without creating additional directories. Create a todo.feature file where we will be placing our test scenarios.

touch todo.feature

Rewriting the Tests

Every feature file should start with a Feature keyword that explains what tests or scenarios that file is testing. We'll start our file with a short explanation:

Feature: A Simple Todo List
    In our busy world, its easy to lose track of what we are working on 
    and what we need to get done. We need a tool that helps us keeps 
    track of our tasks.

To run the scenarios in the feature file, we need a specific Test case. Open up our todo_test.go file and add the following code snippet. Read More

func InitializeScenario(sc *godog.ScenarioContext) {
}

func TestFeatures(t *testing.T) {
  suite := godog.TestSuite{
    ScenarioInitializer: InitializeScenario,
    Options: &godog.Options{
      Format:   "pretty",
      Paths:    []string{"./"},
      TestingT: t,
    },
  }

  if suite.Run() != 0 {
    t.Fatal("non-zero status returned, failed to run feature tests")
  }
}

This code snippet initalizes godog and runs the any features that are located in the .feature files in the current directory. If you placed the features in another directory, you have to update the Paths parameter.

Testing Adding a Task

In our first post, we started with testing and ensuring that we can create a task and add it to our list. Lets do the same. In our .feature file, lets start by adding our scenario:

Scenario: I can add a task

When testing, its important that each test has its own context and does not affect other tests. This means that we need to ensure that our test has all the information it needs and doesnt rely on anything else. In other words, we need to clarify what is the current state of our application and how we expect it to behave.

In our scenario of adding a task - we have to ask ourselves, are we adding to a new empty list? are we adding to an existing list with existing items, etc. Lets enhance our Scenario to cover the former:

Scenario: I can add a task to an empty list
   Given I have an empty list

Now that the context is clear, we can start explaining what needs to be done and whats expected:

Scenario: I can add a task to an empty list
   Given I have an empty list
   When I create a task called "Clean Dishes" to "help the wife clean the dishes after dinner"
   Then the list should have 1 task in it

Creating a task if you remember requires a Title and a Description. In our When step, we did that exact thing by defining the title Clean Dishes and describing the task as help the wife clean the dishes after dinner.

The final step in our scenario is to confirm that the task was added. We just check the length of the list.

You can (and should) enhance the step & implementation to check that the right title & description were added. You can add a Get Task to the service and check the title and the description.

Lets run the test and see what happens:

go test -run ^TestFeatures$

outcome

The outcome is expected, its telling us that our plain english test cases make no sense to go and we have to implement Step Definitions for them.

Lets copy the code from the terminal as is, and add it to our todo_test.go file:

func iCreateATaskCalledTo(arg1, arg2 string) error {
    return godog.ErrPending
}

func iHaveAnEmptyList() error {
    return godog.ErrPending
}

func theListShouldHaveTaskInIt(arg1 int) error {
    return godog.ErrPending
}

//function already exists
func InitializeScenario(ctx *godog.ScenarioContext) {
    ctx.Step(`^I create a task called "([^"]*)" to "([^"]*)"$`, iCreateATaskCalledTo)
    ctx.Step(`^I have an empty list$`, iHaveAnEmptyList)
    ctx.Step(`^the list should have (\d+) task in it$`, theListShouldHaveTaskInIt)
}

Note: InitializeScenario function already exists. Copy the body into it or replace it

If we run the tests again now, we should see progress:

go test -run ^TestFeatures$

1 scenarios (1 pending)
3 steps (1 pending, 2 skipped)
325.1µs

Preparing our task list

Lets implement the steps that we copied in - as they currently are pending as stated in the result:

func iHaveAnEmptyList() error {
    return godog.ErrPending
}

We will simply here overwrite our service and create a new one - deleting any tasks we have created. Make sure you define a global variable first:

var myList MyTodoList

Then update our test implementation to initialize a new list and create a new slice to store our tasks.

func iHaveAnEmptyList() error {
       myList = MyTodoList{Tasks: make([]Task, 0)}
       return nil
}

Adding a task to our list

Our step definition to add a task is as follows:

When I create a task called "Clean Dishes" to "help the wife clean the dishes after dinner"

And our step implementation definition:

func iCreateATaskCalledTo(arg1, arg2 string) error {
    return godog.ErrPending
}

Notice how our step implementation takes two string arguments (arg1, arg2). These conveniently map to our step definition parameters, in our case: (Clean Dishes, help the wife clean the dishes after dinner).

We can rename the function arguments to be easier for us to follow which argument is which, and implement the step:

func iCreateATaskCalledTo(title, description string) error {
    _, err := myList.CreateTask(title, description)
    return err
}

If any failure occurs in the Create task function, the test will also fail for us.

Checking length of our task list

Then the list should have 1 task in it

arg1 below is the number of tasks expected in our step definition - in the example above: 1.

func theListShouldHaveTaskInIt(arg1 int) error {
    currentLength := myList.Length()
    if arg1 != currentLength {
        return fmt.Errorf("wrong number of tasks, expected: %d, found: %d", arg1, currentLength)
    }
    return nil
}

We simply call the length and compare with our expected argument. If the numbers dont match, we show a meaningful message of what we got vs what we expected.

Run the test and verify

go test -run ^TestFeatures$

1 scenarios (1 passed)
3 steps (3 passed)
441.2µs
PASS

Testing Completing a Task

Defining the scenario

We start by defining our requirement in english

Scenario: I can complete a task on my list
   Given I have a list with the following tasks
    | title         | description                     |
    | Clean Dishes  | Help the wife clean the dishes  |
    | Continue Blog | Write part two of the blog post |
   When I complete the task "Clean Dishes"
   Then the list should have 1 task in it

Unlike the first test - we dont start with an empty list. Here we want to assume that tasks have been already created for us. We use Data Tables to define multiple tasks at once.

Data Tables are handy for passing a list of values to a step definition:

Preparing and linking the Steps

We then utilize godog to generate for us the functions that need to be implemented:

go test -run ^TestFeatures$

outcome

Lets copy the code from the terminal as is, and add it to our todo_test.go file:

func iCompleteTheTask(arg1 string) error {
    return godog.ErrPending
}

func iHaveAListWithTheFollowingTasks(arg1 *godog.Table) error {
    return godog.ErrPending
}

//function already exists
func InitializeScenario(ctx *godog.ScenarioContext) {
    ctx.Step(`^I create a task called "([^"]*)" to "([^"]*)"$`, iCreateATaskCalledTo)
    ctx.Step(`^I have an empty list$`, iHaveAnEmptyList)
    ctx.Step(`^the list should have (\d+) task in it$`, theListShouldHaveTaskInIt)

    //add our new step definitions
    ctx.Step(`^I complete the task "([^"]*)"$`, iCompleteTheTask)
    ctx.Step(`^I have a list with the following tasks$`, iHaveAListWithTheFollowingTasks)
}

Implementing the tasks table

Unlike the simple steps where strings and numbers are used in the scenario step, we are using a Data table (godog.Table). This provides us with a simple of way of accessing data in a row & cells or grid format.

To fill the tasks deifned, we have to loop through the rows of our tasks table and insert them in our tasks list.

Row 0   Title (Cell 0)         Description (Cell 1)
Row 1 | Clean Dishes  | Help the wife clean the dishes  |
Row 2 | Continue Blog | Write part two of the blog post |

Note that the column headers we used (title, description) are also considered a row. So we have to make sure we dont start at index 0 when inserting the tasks.

func iHaveAListWithTheFollowingTasks(arg1 *godog.Table) error {
  //clear the task list
  myList = MyTodoList{Tasks: make([]Task, 0)}

  //make sure we skip the first row of titles, 
  //and start at row 1 of the acutal data
  for _, row := range arg1.Rows[1:] {
    //create a task using the title in Cell 0 and the description in Cell 1
    if _, err := myList.CreateTask(row.Cells[0].Value, row.Cells[1].Value); err != nil {
      return err
    }
  }
  return nil
}

We first clear the tasks list to make sure that we dont have other tasks in our list. Our step says that we only have the lists defined in the table in our task list. If we dont clear the tasks, we could have residual tasks from other steps, breaking our tests.

Implementing the completion step

When I complete the task "Clean Dishes"

There is one argument in our step, thats the task name we need to complete. You can rename arg1 to taskTitle if you wish to be more descriptive.

func iCompleteTheTask(arg1 string) error {
    return myList.CompleteTask(arg1)
}

If the completion fails for any reason, so will our test.

Run the test and verify

go test -run ^TestFeatures$

2 scenarios (2 passed)
6 steps (6 passed)
644.8µs
PASS

Whats Next

This post will be built upon in future posts to enhance the features and eventually become a fully fledged tutorial. Not sure when, but the next steps are:

  1. Introduce directories to follow a "cleanish" architecture setup
  2. Introduce new methods of storage for the list (inmem, db)
  3. Introduce ports/interfaces for the service (http server & CLI)