BDD where it earns its place, and nowhere else
I have a slightly complicated relationship with BDD. I've watched it turn a tangled test suite into something the whole team could read and reason about, and I've watched it turn a perfectly good unit test into a paragraph of ceremonial English that nobody benefits from. So when go-tool-base brought in Cucumber-style BDD, the interesting decision wasn't adopting it. It was being ruthless about…
The story of Behavior-Driven Development (BDD) at go-tool-base illustrates the importance of strategic application of any development framework. Initially, BDD was introduced to the team via Cucumber-style tools, but the decision to use it judiciously rather than unwaveringly proved to be a turning point. Most of the team's tests were straightforward table-driven Go tests, which worked perfectly well without any additional layer of abstraction.
However, there were two areas that suffered due to overly complex and hard-to-understand test suites.
One area was the pkg/controls package, which handled a state machine and various functionalities like signal handling, health monitoring, restart policies, and graceful shutdown. The integration tests for graceful shutdown had expanded to over three hundred lines of code, filled with complex goroutine and channel coordination. These tests worked, but they were challenging to review and trust, as the complex synchronization logic obscured the simple fact that "when a shutdown signal arrives mid-startup, the controller stops cleanly."
The second problematic area was the CLI itself, featuring commands like init, update, and doctor. Each command had specific user workflows, such as checking whether a custom value persists after running the init command. Initially, these workflows were written as Go code, but they could have benefited from BDD's readability. By using Godog, a Go implementation of Cucumber, the scenarios became more readable and understandable to anyone, including non-Go developers from the operations team, without needing to understand Go code.
What set go-tool-base apart was their strategic decision to use BDD only where it added clarity and readability, keeping table-driven Go tests as the default approach for other cases. They drew a clear line between the narrative scenarios (where BDD paid off) and matrix tests (where BDD was unnecessary). Feature files were placed in the root 'features/' directory, step definitions in 'test/e2e/', and unit tests untouched, as they were already the optimal tool.
The success of this approach hinged on two smaller decisions: running BDD scenarios with 'go test', eliminating the need for separate Cucumber runners, and ensuring BDD tests were integrated seamlessly into the existing CI pipeline. The CLI end-to-end tests also reused a single compiled binary, avoiding the performance hit of recompiling for each scenario.
In summary, the key takeaway is to apply BDD as a scalpel, not a religion. BDD is most effective for scenarios involving ordered steps with meaningful state in between, while matrix tests benefit from the simplicity of table-driven Go tests. By following this rule, development teams can harness the power of BDD without falling prey to its overuse and potential drawbacks.
Written by urgent.news from Dev.to's reporting — not their text. Machine-written — may contain errors; check the original before relying on it.