March 23rd, 2024
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.
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
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.
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$
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
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
}
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.
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.
go test -run ^TestFeatures$
1 scenarios (1 passed)
3 steps (3 passed)
441.2µs
PASS
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:
We then utilize godog to generate for us the functions that need to be implemented:
go test -run ^TestFeatures$
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)
}
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.
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.
go test -run ^TestFeatures$
2 scenarios (2 passed)
6 steps (6 passed)
644.8µs
PASS
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: