Learning better testing using BDD

24 Feb 2024

As a developer traversing time and space over the last 10 years, I've seen some trends come and go. Around the early 2010's, as TDD was reaching it's plateau of productivity, I came across a software tool called 'cucumber'. It was a magical tool that let you write bits of code that automate your frontend. A technical product manager, would then compose bits of this to code their own acceptance criteria and tests for the app. Now in 2024, I'm seeing less dialogue about this type of tool. Is it because it's shit? Or are there better options? Now, I think there is merit to the idea, but we can leave behind some of the complexity and heavy weight tools. It is also important to consider how many end to end tests should be supported.

This blog post delves into Behaviour-Driven Development (BDD), exploring how we can embrace its core principles for clear communication and collaboration without succumbing to the allure of overly complicated frameworks.

Bridging the Communication Gap:

The value of testing is undeniable. However, traditional unit testing, integration testing, while essential, can sometimes feel undervalued by the non-programmers in your team. Maybe at some stage you've struggled to communicate with stakeholders, project managers or customers the value of these tests. A typical test, is a simple function with a number of actions taken, ultimately a crash of the test, or a 'assert' that fails will result in a failed test. You might see the output of your tests as a series of red/green lines in a text file report. Tools like junit will show a structure that has a class and method names in the report. Although these tests may be hugely valuable for the developers, they may as well be complicated hieroglyphs to others.

The BDD way

BDD bridges the comms gap by establishing a ubiquitous language between devs and stakeholders.

The most extreme version of BDD involves the use of a domain specific language to agree usecases with the stakeholders. We then code up exactly this test in the app, and can point to a green line in a report to communicate that we have exactly developed this feature, and it's working as intended! It builds up a valuable asset for understanding what the system does too. New people will understand the breadth of a feature. You will be able to see how features break each other. Furthermore, if you use a tool like (gherkin/cucumber) then the test may even be able to be composed mostly from reused chunks of domain language.

Here's what an example test might look like in the cucumber BDD style DSL

Given a user is logged in
When they click the 'add to cart' button
Then the cart should contain the item

The test above is broken in to three parts, given/when/then. The first line is our test setup, which provides all the preconditions of what the test does. Following, we have a 'When' statement, which represents some kind of action that the user takes. Finally, we have a 'Then' statement, which represents the expected outcome of the test. This simple and clear description of the feature has some underlying code that will run the test, and report back to the user if the test passed or failed. Each of the lines in this test can be composed and remixed to create a new test.

For example, we can add a new test case by implementing just the 'then the car should be empty' part of the test. This is a powerful way to build up a suite of tests that describe the functionality of the app.

Given a user is logged in
When they click the 'add to cart' button
When they click the 'checkout' button
Then the cart should be empty

In my experience, this approach can work really well with the right team. However, it's not without problems. Often, the underlying technology of driving your app is a janky nightmare. In web apps this will be a browser driving tool like playwright or selenium. In desktop applications you might have nightmarish OCR tool like sikuli. Over time, you build up many many tests to describe the functionality. Because you are driving the tests end to end, they can be slow to run, involve complex setup and be flaky due to underlying changes in the user interface. The process of adding new tests is hyper optimized and easy thanks to the domain language, but the act of maintaining the tests gets harder and harder. It makes your momentum on changing the app slow down, and the tests become a major time sink. What compounds with this problem is that the tests become a contract with the business, where you need to be dogmatic about having new coverage with each feature, and never breaking backwards compatibility.

What we can learn and take in to our own projects

Despite all this pain, there are good parts of BDD. I don't recommend the heavyweight frameworks like gherkin or cucumber. I do think you should write some end to end tests with playwright/selenium. So what should we take from BDD in to our every day practice?

  • Shared language: We should get credit for delivering tests as part of feature development. Use the shared domain language to communicate clearly what the software will do, and build this in to the tests so that we have a solid way of communicating the behaviour of the software when it's 10 months down the track and no one remembers how the feature works.
  • Single purpose tests: Each test focuses on a single specific scenario, improving test clarity and simplifying maintenance.
  • Given/When/Then structure: This structure provides a clear and concise way to define the test scenario, ensuring everyone understands the setup, action, and expected outcome.
    • Given: Sets the initial state of the system.
    • When: Describes the action which we are testing.
    • Then: Defines the expected outcome of the action.

These seemingly simple principles can significantly enhance your existing testing practices. By embracing the shared language and focusing on collaboration, you can build better software faster. A consistent approach to these tests are really helpful for yourself when you are coming back to the feature 12 months down the line. They can be helpful for new developers to see what a feature actually does. If you have technical thinking non-programmers in the team, they will love the ability to specify and understand functionality by looking at the way it is tested.

BDD isn't about forcing rigid automation onto non-technical stakeholders. Nor is it about making your developers work with brittle and annoying test frameworks. It's about fostering clear communication and shared understanding throughout the development process. By embracing the core principles of BDD and adapting them to your workflow, you can build stronger relationships with stakeholders, improve collaboration, and ultimately, build better software faster.

Remember, the key is to find the approach that works best for you and your team. Happy coding!

Subscribe to my Newsletter

Want to know more? Put your email in the box for updates on my blog posts and projects. Emails sent a maximum of twice a month.