How Using Cucumber tests can accelerate your development process

author By Paul Schalkwijk (Senior Software Developer)

Introduction to behavior-driven development (BDD) and the Cucumber framework

Our team embarked on a quest to create software that precisely meets business objectives. This effort focused on integrating behavior-driven development and the Cucumber framework into our workflow. BDD is a development methodology that enhances communication between technical teams and business stakeholders by defining requirements in clear, simple language. Cucumber, a tool that supports BDD, allows these human-readable specifications to be converted into automated tests. This brief exploration sets the foundation for understanding how these practices can revolutionize development workflows, fostering better collaboration and efficiency.

Understanding BDD: Bridging the gap between business and development

Behavior-driven development plays a crucial role in translating business requirements into technical specifications by fostering a collaborative environment where developers, QA engineers, and non-technical stakeholders can work together seamlessly. This collaboration is achieved through the use of a common language that describes the functionality of the system from the perspective of its users. BDD's emphasis on outcomes, rather than technical details, helps ensure that the development efforts are directly aligned with business objectives and user needs.

This example of BDD in practice can illustrate how this approach streamlines the translation of requirements into a technical solution. Consider a scenario involving an API that handles charity donations, where the business requirement is to enable users to search for charities, make donations and receive tax receipts via email. Here's a streamlined example of a Cucumber feature file for this scenario:

Feature: Charity Donation Processing
  Scenario: Making a Donation
    Given a charity exists with the name "Clean Oceans"
    When I submit a donation of "$50" to "Clean Oceans"
    Then the donation should be processed successfully
    And a tax receipt should be generated and sent via email

This example maintains a focus on the user's interaction with the system without delving into the specifics of the underlying API implementation. By specifying the behavior in terms of actions and outcomes — searching for a charity, making a donation and receiving a tax receipt — the team can develop and test against these scenarios to ensure the software meets the defined business requirements.
Glue code in Cucumber is the Java code that connects Gherkin steps to automation code. We define this glue code in various StepDefinitions classes. Using annotations like @Given, @When and @Then, it translates the human-readable scenarios into actions that test the application. Writing effective glue code involves crafting concise methods that are both specific to the steps and reusable across scenarios, making the development process more efficient and aligned with the intended software behavior.

@When("a credit card donation of {int} {currency} is made for giving opportunity {giveOp} by user {user} as {string}")
    public void recordCreditCardDonationToCause(final int donationAmount,
            final Currency currency,
            final GivingOpportunity giveOp,
            final User user,
            final String donationAlias) throws IOException {
        final DonationTransactions donation = donationClient.recordCreditCardDonationToGivingOpportunity(
                user.getUserAlias(),
                user.getCompanyId(),
                currency,
                donationAmount,
                giveOp.getId());
        scenarioContext.setContext(donationAlias, donation);
    }

This example shows how we can translate a feature description into an actual behavior. We call the API using one of our per-domain clients, which abstracts the logic required to record a credit card donation. We use a scenarioContext class to store information we might need in follow up steps. BDD thereby acts as a bridge, translating business requirements into actionable, testable scenarios that help guide the technical development process.

Streamlining BDD with Cucumber and Dockerized TestContainers

Integrating Cucumber with Dockerized TestContainers revolutionizes our BDD approach by ensuring tests run in environments that mirror production settings. This setup is particularly beneficial for teams that have Dockerized their services, as it allows for seamless integration testing across various (micro)services.

Steps for implementation:

  1. Dockerize your services. Containerize your application services. Each service should define the environment, properties and how the service is run. You can create Dockerfiles for this; we use and configure many of the pre-built modules provided by the TestContainers library.
  2. Integrate TestContainers with Cucumber. In your test suite, add the TestContainers Java library (or equivalent for your programming language). This library allows your tests to control Docker containers programmatically.
  3. Configure the test environment. In your Cucumber setup, configure TestContainers to start the necessary Docker containers before the test scenarios begin. This may involve specifying Docker images for your services, any required environment variables and port mappings.
  4. Write test scenarios. Using Gherkin syntax, write your BDD scenarios that describe the expected behavior of your application. These scenarios will be executed against the services running in Docker containers.
  5. Execute and validate tests. Run your Cucumber test suite. TestContainers will ensure that the appropriate Docker containers are spun up for the environment required by your tests. After the tests complete, ensure containers are automatically stopped and removed, ensuring a clean state for subsequent test runs.
  6. Continuous integration (CI). Finally, integrate this testing setup into your CI pipeline to automate the execution of your BDD scenarios. This ensures that every build is automatically tested in an environment that matches your production setup as closely as possible.

 

We achieve this with an override of the Cucumber ObjectFactory. Here we can define a configuration for dependency injection called ServiceModule. This module in turn defines a CucumberSerlvetExtension — the class we use to configure and start the necessary Docker containers. With this custom entry point defined, when using any classes defined in the dependency injection configuration in your StepDefinitions, Cucumber handles the rest.

Cucumber Code

The key benefits of this approach include:

  • Consistency. Matches test environments with production, reducing discrepancies.
  • Isolation. Tests run in isolated environments, eliminating interference.
  • Flexibility. Easily test against different configurations and external services.
  • Scalability. Spin up only what you need, when you need it, optimizing resources.

API first and integration tests: A perfect match for BDD and Cucumber

Embracing an API-first approach ensures that our APIs are well-designed and meet business requirements from the start. Integrating this approach with behavior-driven development and Cucumber offers a compelling strategy for developing precise, reliable integration tests.

In an API-first context, Cucumber excels by shifting the focus from UI-based to API-based testing, addressing the common issues of flakiness and maintenance challenges associated with UI tests. This method simplifies defining clear, behavior-driven test scenarios, making API testing more accessible and understandable across the development team.

Key takeaways:

  • Improved test reliability and efficiency. Focusing on APIs reduces the complexity and instability often associated with UI testing. It allows for more consistent, predictable testing outcomes, speeding up the development cycle and reducing the time to market.
  • Enhanced collaboration across teams. The use of common, understandable language in test scenarios bridges the gap between technical and non-technical team members. APIs are often highly technical, so using BDD here fosters a collaborative environment where stakeholders have a clear understanding of project goals and progress.
  • Business and technical alignment. This method guarantees that the development work is in direct alignment with business objectives, ensuring that the final product not only works flawlessly but also delivers real value to users.

API

We can capture API requests and responses for each step, and use them for documentation or analyze them in case a test fails.

Overcoming initial challenges, and enhancing development with Cucumber and BDD

Initially, we faced challenges with reportability. However, by integrating Cucumber reports into our continuous integration pipeline, we overcame this hurdle and now benefit from actionable insights and metrics that guide our development efforts. This integration has provided our team with clear visibility into test outcomes, helping us to continuously improve test coverage and quality.

An ongoing challenge is translating business requirements into code, and ensuring that these translations remain clear to all stakeholders. This is a fundamental challenge in software development, and BDD helps — but does not take it away completely.

A technical challenge you will need to deal with is preventing state leakage between test scenarios. State leakage can cause unreliable test outcomes, where the result of one test affects another. To combat this, it's crucial to isolate test scenarios from each other. This can be achieved through practices like using Docker containers to create independent test environments, or applying transactional rollbacks in database tests. Ensuring each test runs in a clean environment helps maintain the integrity and reliability of the testing process.

Introducing Cucumber during dedicated innovation time meant that adopting this new framework didn't detract from our feature development schedule. As a result, we've been able to maintain our roadmap progress while integrating BDD practices. Now fully implemented, this framework has not only accelerated our feature development but also enhanced the quality of our output. Test scenarios now guide our development, ensuring that new features meet both the technical specifications and the business needs they were designed to address.


Reporting shows how we are increasing our coverage over time.

Reporting shows how we are increasing our coverage over time.

Conclusion

By adopting Cucumber and behavior-driven development as part of our “personal innovation” project, we've significantly streamlined our development process. This approach has allowed us to present test cases in a format that is understandable to non-technical staff, facilitating more meaningful discussions and collaboration. The ability to reference specific test scenarios has become a powerful tool for quickly answering questions about software functionality and resolving ambiguities in requirements.

This journey underscores the value of investing in process innovation. By dedicating time to adopt and integrate Cucumber and BDD into our workflow, we've created a more agile, collaborative and efficient development environment that delivers high-quality software that aligns with our business objectives and user expectations.