What is Gherkin?
An Introduction to Behavior-Driven Development (BDD) with Gherkin
In this two-part blog, we will explore the power of Gherkin, a domain-specific language designed for behavior-driven development, and how it can be leveraged in both backend and frontend testing.
Gherkin offers a simple and structured syntax that enables teams to describe the behavior of a system in a way that is easily comprehensible to both technical and non-technical stakeholders. It uses keywords like “Given,” “When,” “Then,” and “And” to define different parts of a scenario. “Given” outlines the initial state of the system, “When” describes the action being taken, and “Then” specifies the expected outcome. Additional steps can be added using the “And” keyword.
The structure of a Gherkin scenario consists of a Feature, which describes the functionality being tested, and one or more Scenarios that detail specific test cases for that functionality. Each Scenario is defined using Gherkin keywords, making it easy to read and understand.
One of the key advantages of Gherkin scenarios is their ability to automatically generate test cases, saving time and increasing efficiency. Their user-friendly format allows everyone on the team to contribute to the testing process, including non-technical stakeholders like business analysts and project managers. Furthermore, Gherkin scenarios can also serve as documentation for the system’s behavior, making them valuable for future reference and onboarding new team members.
We will delve into the pros and cons of implementing Gherkin tests in both backend and frontend development. Whether you’re new to BDD or looking to enhance your testing approach, this blog will provide insights to help you make informed decisions and improve collaboration within your team.
Part 1: Backend Test Automation in Elixir
There are several libraries available for using Gherkin in different programming languages. In Elixir, one of the most popular libraries for Gherkin is Cabbage.
Cabbage is similar to Cucumber, a well-known Gherkin library in the Ruby world, and provides the necessary tools to parse Gherkin feature files, execute the scenarios, and run the tests. With Cabbage, you can write feature files using Gherkin syntax and then define step definitions in Elixir to implement the scenarios.
One of the advantages of using Cabbage is its integration with other testing frameworks such as ExUnit and Erlang’s Common Test. This allows you to easily incorporate Gherkin scenarios into your existing test suite and run them alongside other tests.
Another benefit of using Cabbage is its flexibility in defining step definitions. You can define step definitions using regular expressions or functions, which gives you the ability to write step definitions that are more concise and expressive.
However, it’s worth noting that Cabbage is not the only library available for using Gherkin in Elixir. Other options include Hound and BDDex, which provide similar functionality for working with Gherkin feature files in Elixir projects. Ultimately, the choice of the library will depend on the specific needs of your project and the preferences of your development team.
In this backend test automation approach, we will rely on the utilization of factory methods to create consistent and reliable test data. By leveraging these factory methods, we can ensure that our tests are executed on the application level, thoroughly validating the behavior and functionality of our backend systems. This allows us to simulate real-world scenarios and ensure the robustness of our applications.
We’ll explore the utilization of Gherkin scenarios with the Cabbage library, revealing how they can enhance the efficiency and reliability of your testing process, all while harnessing the capabilities of the Elixir programming language.
Using Gherkin with Cabbage in an Elixir project is relatively straightforward. Here is an example of how you can set up and use Gherkin with Cabbage in an Elixir project:
1. Add Cabbage as a dependency in your project:
You will need to add Cabbage to your list of dependencies in the mix.exs file.
def deps do [{:cabbage, "~> 0.3.0", only: :test}] end
- :cabbage is the name of the dependency. This is the name that will be used to refer to the dependency in the project code.
- “~> 0.3.0” is a version requirement for the dependency. It specifies that the project requires a version of cabbage that is greater than or equal to 0.3.0, but less than 0.4.0. The ~> operator is used to specify a compatible version range.
- only: :test is an option that specifies that the dependency should only be included when running tests. This means that the dependency will not be included in the project when it is compiled or released.
2. Create a features directory:
This is where you will store your Gherkin feature files. It is a good practice to keep your feature files organized by creating subdirectories for different features.
3. Write your feature files:
Once you have created a Gherkin file using a “.feature” extension, you can start writing scenarios. Each scenario should start with the Scenario keyword, followed by a descriptive name for the scenario. The purpose of the scenario is to describe a particular feature or functionality being tested. Within the scenario, you can use the Given-When-Then keywords to define the preconditions, actions, and expected outcomes for the scenario.
In this example, we can see the ‘Hospital Management Service’ Feature with the scenario of adding a patient to the system.
Feature: Hospital Management Service The Hospital Management Service feature allows users to easily add new patients to the system, providing essential information such as name, age, gender, address, and contact details. The system securely saves the patient's record in the database and returns a success message to confirm the successful addition. This feature streamlines patient management and enhances overall operational efficiency in the hospital. Scenario: Register a new patient in the hospital management system Given the following list of patients and their information | Name | Age | Gender | Address | Contact | | John Doe | 32 | Male | 123 Main St, Anytown USA | 555-555-5555 | | Jane Doe | 28 | Female | 456 Park Ave, Anytown USA | 555-555-5556 | | Bob Smith | 40 | Male | 789 Elm St, Anytown USA | 555-555-5557 | When a hospital worker submits a request for adding 'John Doe, Jane Doe, Bob Smith' as a list of patients Then users should be registered in the Hospital Management System And there should be a message indicating that 'Patients were successfully added'
Given – all prerequisites for the test should be satisfied, in this case, we have all the necessary information that the system needs to enter patients.
When – it is used to initiate an action, in our case the user initiates saving the user to the hospital system.
Then – the final step serves to verify the desired test output. A success message is returned by the system that the patients were successfully saved.
And – in this case is actually an additional Then step, which performs the second test output verification, in our case the users were successfully saved to the hospital database.
4. Write step definitions:
Match the steps in your feature files and define the actions that should be taken when each step is executed. In the following example, steps will be written in Elixir.
defgiven ~r/^the following list of patients and their information$/, %{table: patient_info}, %{context: context} = state do patient_info = patient_info |> Enum.map(fn row -> row |> Map.take(["Name", "Age", "Gender", "Address", "Contact"]) |> Map.new() end) context = context |> Context.set_patient_info(patient_info) {:ok, %{context: context}} end
This step definition takes a table of patient information and stores it in the context. The table is transformed into a list of maps with only the necessary fields, and the resulting patient list is stored in the Context using the Context.set_patient_info function.
The Context is used to store the state of the operation being performed, which is being updated as various functions are called, and operations are performed on the schedule.
defwhen ~r/^a hospital worker submits a request for adding 'John Doe, Jane Doe, Bob Smith' as a list of patients$/, %{context: context} = state do {:ok, patient} = Context.get_first_patient(context) response = HospitalManagement.add_patient_to_system(patient) context = context |> Context.set_last_response(response) {:ok, %{context: context}} end
This step definition simulates sending a request to add the patient to the system using the HospitalManagement.add_patient_to_system function. It gets the first patient from the patient list stored in the context using the Context.get_first_patient function. The resulting response is stored in the context using the Context.set_last_response function.
defthen ~r/^users should be registered in the Hospital Management System$/, %{context: context} = state do response = Context.get_last_response(context) assert response.status == :ok assert response.message == "Patient added successfully." {:ok, %{context: context}} end
This step definition asserts that the last response received from the system has a status of :ok and a message of “Patient added successfully.” It uses Context.get_last_response function to get the last response stored in the context.
defthen ~r/^there should be a message indicating that 'Patients were successfully added'$/, %{context: context} = state do {:ok, patient} = Context.get_first_patient(context) assert HospitalManagement.patient_exists_in_database?(patient) {:ok, %{context: context}} end
This step definition asserts that the patient’s information has been saved to the database using the HospitalManagement.patient_exists_in_database function. It gets the first patient from the patient list stored in the context using the Context.get_first_patient function.
Pros and Cons:
Pros:
- Improved collaboration: Implementing Gherkin tests in the backend allows for better collaboration with non-technical stakeholders, such as business analysts and project managers, as the plain text format is easy to read and understand.
- Clear and concise requirements: Gherkin’s “Given-When-Then” format helps to clearly outline the requirements for each scenario in the backend. This ensures that all necessary scenarios are covered and easy to comprehend and maintain.
- Modular tests: Gherkin tests can be modular in the backend, allowing for reuse across different applications and environments, and saving time and effort when testing similar functionalities in various projects.
- Supports test automation: Backend Gherkin tests can be automated using tools like Cucumber and ExUnit, enabling efficient execution of repetitive tests, reducing human error, and improving test coverage.
Cons:
- Additional complexity: The implementation of Gherkin tests in the backend can introduce extra complexity, requiring the definition of step definitions, maintenance of feature files, and alignment with application changes.
- Steep learning curve: Writing and maintaining Gherkin tests for the backend may demand a certain level of technical expertise, which could be challenging for non-technical stakeholders unfamiliar with these tools.
- Limited flexibility: Backend Gherkin tests are constrained to the scenarios and steps defined in the feature files, potentially making it difficult to test complex scenarios that do not fit the step-by-step format.
- Maintenance overhead: As the backend application evolves, Gherkin tests must be updated accordingly, which can be time-consuming, especially for large and complex applications.