Go with Cucumber: An Introduction for BDD Style Integration Testing

Sofian Hadianto
tiket.com
Published in
7 min readMay 19, 2023

In this article, I aim to describe my journey of learning integration tests in a golang environment and illustrate how we can utilize a BDD tool called Cucumber Godog to document the features of our golang code. By doing so, we can create living documentation that stays up-to-date as the code evolves, while also capturing the shared understanding of our team regarding the functionality of the code.

To demonstrate these concepts, I will present a straightforward application that will serve as the target for our tests. This application functions as a basic CRUD API for managing books, utilizing a single table known as the “book” table. The API itself is implemented in golang, utilizing the gofiber framework. Although this case study represents a simplified scenario, it can effectively showcase a real-life situation.

.
├── database
│ └── database.go
├── go.mod
├── go.sum
├── main.go
├── models
│ └── models.go
├── routes
│ └── routes.go

Tools

We need to install several tools, including:

  1. Cucumber Godog
  2. Testcontainer
  3. Docker

What is Cucumber Godog ?

Godog is the authorized BDD (Behavior-Driven Development) framework for golang, specifically designed for Cucumber. This framework parses feature specifications written in plain text, referred to as feature files, and verifies the behavior of the system.

While this article discusses the Godog framework, it does not provide an in-depth explanation of how BDD works with this tool.

Godog employs a syntax called Gherkin, which consists of a set of grammar rules facilitating the creation of scenarios. These scenarios are stored in text files with the “feature” extension and encompass various keywords that define complete tests, such as pre-conditions, test descriptions, expected outcomes, and more.

To illustrate the implementation process, we will refer to a sample feature file as a foundation in this article.

Feature: Book management
In order to use book API
As a Librarian
I need to be able to manage books

Scenario: then user try to insert one book, one created book should be displayed by the system
When I send "POST" request to "/books" with payload:
"""
{
"id": 1,
"title": "Dune",
"author": "Frank Herbert"
}
"""
Then the response code should be 201
And the response payload should match json:
"""
[
{
"id": 1,
"title": "Dune",
"author": "Frank Herbert"
}
]
"""

Here are some of the common keywords used in Gherkin:

Feature

Each Gherkin file starts with a keyword Feature. It describes what the system is supposed to do. This keyword is also used to group multiple scenarios. We can also write a description underneath the given keywords as long as it does not contain any reserved keyword.

Scenario

This keyword represents a scenario in Gherkin. It includes all the possible circumstances of a feature.

Given

This keyword is used to describe the pre-requisites before a test can start. When godog runs a Given step, it will configure the system under test to be an already defined state, such as adding required data to the database. We can have multiple Given steps if required.

When

This keyword describes an action or something that happens to the system that will cause something else to happen, an outcome.

Then

Is an outcome or behavior that we expect from the system when this action happen in this context.

And

When we have multiple steps for Given for example, we can chain the context using this keyword. This keyword will improve readability for our scenario.

Testcontainer

To ensure the tested application functions properly during testing, it requires a running database. Therefore, we utilize the testcontainer tool to initiate a database instance when executing the tests. While it is possible to employ a separate database exclusively for testing purposes, this approach often demands significant resources and lacks isolation. Consequently, it is more convenient to utilize testcontainer. This tool offers an integration testing environment that allows us to concentrate on the test scenarios and rely on the container to provide the necessary dependencies for the system being tested. For further information, you can visit this link: https://golang.testcontainers.org/.

How to

Now let’s start creating test for above feature file. We will develop the integration test that will check if submitted book created in database or not.

Let’s install the dependencies first.

go get -v github.com/cucumber/godog/cmd/godog@latest
go get -v github.com/testcontainers/testcontainers-go
go get -v github.com/testcontainers/testcontainers-go/wait

Then create our feature file in the/tests/features folder and create two files inside /tests called book_test.go and book_step_definition.go. The first file will be responsible for a test launcher and the second will be responsible for test guidance.

Now, try to run the test scenario by typing this command.

go test -test.v -test.run ^TestFeatures$ ./tests

You will see following results

Godog gives us the summary containing 1 scenario and 3 steps all undefined. Undefined meaning godog doesn’t know what to do for any of the 3 steps we wrote in the feature file. It needs to be supervised by step definitions. Step definition will translate plain language in feature file using Gherkin into golang code then annotated with a pattern. When godog runs the steps, it looks for step definition, if found godog then executes the method.

We can see that Godog helps us to print out the code snippet that we can use as a basis for step definition, lets’s copy them into a file called step_definiton.go under the tests folder and called InitializeScenario inside TestFeatures method. Let’s run the test again, it still failed since we have not written any logic that support our steps.

We can see that our scenario still pending and we have 1 step in pending and 2 steps skipped. Seeing our failing test is a milestone, and also proves that it is capable of detecting errors in our code. Next step, all we have to do is implement the action to make it do what it is supposed to do. The pending status means that this step needs supporting logic in order to run the tests. The step “When I send “POST” request to “/books” with payload:” need implementation to send the request to specific URL.

Before we define the step definition, we need to refactor the step definition snippet to support database connection as follows:

Next step is to update code for When step, we will parse the payload from feature file then send HTTP POST request to /books then stored the result in response struct.

In this step, godog will make an HTTP call with the post method to the url /books by sending the new book data in json form and fetching the HTTP status code and response body into a response helper object, and saving it in context ctx so that it can be used in the next step.

Then run the test again, the previous pending test will be green and Then step will be pending.

Now, we can create supporting code for the Then step.

This code will fetch the response from the previous HTTP call using ctx.Value(godogsResponseCtxKey{}).(response) and then compare the HTTP status from this call with the expected HTTP status from the feature file. If not equal, the function will throw an error assertion.

Then let’s rerun the test again to see the result. Now, the Then step become green.

Let’s continue to implement the last step for this scenario. The And step will compare the expected JSON from the feature file and the actual JSON response from the previous HTTP call. If equal, we can say that the step is passed and we have no other steps left.

Finally, all steps in our test scenario are already green or successful.

This is just a sample application demonstrating how to get started with Cucumber Godog in golang, and how we can use this when documenting our system behavior using BDD tools.

Conclusion

The tools presented in this article combine the documentation of system features and tests into a single package, ensuring their coherence. When a feature undergoes changes, it becomes necessary to update the corresponding tests to maintain consistency between the documentation and the tests. The tool facilitates this verification process. Furthermore, the testing scenarios become more readable as they closely resemble human language. This testing technique can also be utilized to document the behavior of legacy systems.

However, there are certain drawbacks to consider. Implementing the steps of the testing scenarios may prove more complex than implementing the feature being tested. Additionally, it is not always possible to containerize all system dependencies. Consequently, during testing, there might be instances where we need to interact with non-isolated external systems, potentially leading to inconsistent test results.

Here is the Github for this code :

If you are interested with the implementation in Java or Python, you can check the following repositories:

--

--