Most teams run BDD scenarios as integration tests through the UI or API. It seems logical: Gherkin describes high-level behavior, so run high-level tests.
But what if that assumption costs you 10 minutes or even hours of feedback time, brittle infrastructure, and constant debugging?
I propose an approach that flips the script: BDD at unit level.
Instead of running scenarios through the full system, validate the business logic directly using the same Given-When-Then scenarios—but at the unit level.
Counter-intuitive? Yes. Faster, clearer, and easier? Absolutely.
The approach BDD at unit level means implementing business logic directly as pure functions and validating them at unit level using Gherkin scenarios, rather than at the end-to-end level.
Benefits include:
- ⚡ 3-second feedback instead of 10 minutes
- 🧩 Surprisingly easy to set up and maintain
- 🔍 Crystal clear failures that pinpoint exact issues
This is Part 1 of a 4-part series:
- Part 1: The Core Idea (you are here)⬅️
- Part 2: Motivation & History (coming soon)
- Part 3: A Concrete Example (coming soon)
- Part 4: Integration Aspects (coming soon)
BDD (Behavior-Driven Development) helps teams specify system behavior in plain language (Given-When-Then scenarios) so business stakeholders, testers, and developers share understanding.
The standard approach:
- Write feature files collaboratively
- Run scenarios through the UI or API
- Get comprehensive end-to-end validation
This creates living documentation. Great!
But here's the costly assumption most teams make: "High-level scenarios require high-level, end-to-end tests via UI or API."
That assumption leads to slow feedback, brittle tests, and complex infrastructure — undermining BDD's core benefits.benefits.
What if you could get the same clarity... in 3 seconds?
This approach applies the Tetris Principle: test every aspect as low as possible in the test pyramid.
Instead of running BDD scenarios through the full system (top of the pyramid), drop them down to the unit level—where they're faster, clearer, and easier to maintain.
[picture]
Here's what makes it work:
To test business logic at unit level, you first need to make it testable in isolation. But how?
The answer is simpler than you might think: pure functions.
A pure function takes input, applies business rules, and returns output—with no side effects. No database calls, no HTTP requests, no file system access. Just logic.
Think of it as "JSON in, JSON out."
Let's use a concrete example: calculating per diem allowances for business travel in Germany (the infamous "Mehrverpflegungsaufwandsentschädigung"—yes, Germans invent long compound words 😆).
This calculation doesn't need to know about cloud storage or REST APIs—just trip duration and location. Pass those in, get the calculated amount back. That's it.
When your logic lives in pure functions, it becomes trivially testable at the unit level. No mocking, no test databases, no complex setup. Just call the function with test data.
This is where Clean Architecture and Hexagonal Architecture come into play: push all environment interactions—cloud storage, databases, external APIs—into adapters at the edges. Keep your business logic pure in the center.
This architectural separation is what makes BDD at unit level possible.
Note: Part 3 will demonstrate this with a complete implementation of the German per diem calculator.
When using Cucumber to express scenarios for the per diem calculation, the best practise is that feature files describe business behavior without any reference to UI or API. They focus soley on the business aspect:
Scenario: Short business trip
Given I am travelling in Germany
And I will be away for 17 hours
When I calculate my per diem allowance
Then I should receive 14 EUR totalThis scenario works at any testing level. What changes is the glue code—the step definitions that connect Gherkin steps to your implementation.
At the integration level (traditional BDD), step definitions:
- Launch browsers or make HTTP requests
- Interact with databases
- Require deployment to a test environment
- Take minutes to execute
At the unit level (the approach we're advocating here), step definitions:
- Set up test data in memory
- Call your pure functions directly
- Run locally without any infrastructure
- Execute in milliseconds
So the implementation is dramatically less complex.
BDD at unit level is counter-intuitive and non-standard but it delivers powerful advantages:
First and foremost, tests run in 3 seconds instead of 10 minutes (or hours). No deployment needed, no waiting for CI pipelines. Developers get instant validation on their local machines.
The perfect #DeveloperExperience 💯, especially compared to the common practice of having the acceptance tests running after a pull request—which results in multiple days to receive feedback.
Stakeholders can see business logic "in action" within hours or days, not weeks. This enables:
- Quick exploration and validation of business rules with stakeholders
- Better shared understanding between technical and non-technical team members
- Earlier detection of business logic bugs and misunderstandings
- Simpler business logic
This even allows for rapid prototyping of business logic.
Yes, classical BDD promises living documentation, too. But here's the reality: when scenarios run as slow end-to-end tests, they become a maintenance burden, teams stop updating them, and then documentation drifts from reality.
At the unit level, scenarios are fast and easy to run. Developers actually use them. They stay current. The business logic and its documentation evolve together in one canonical place, making it easy to:
- Discuss and change requirements with stakeholders.
- Ensure alignment between code and business intent.
- Focus the full team on the scenarios as the backbone of development.
No brittle infrastructure. No complex test environments. No flaky UI tests. No deployment pipelines just to validate business rules, no waiting for the availability of the test system. The infrastructure complexity simply disappears.
Cleaner separation of concerns. During implementation, developers can cleanly separate:
- Business logic changes (pure functions at unit level)
- Integration changes (adapters and infrastructure)
- UI changes (presentation layer)
Each change is isolated, easier to reason about, and cheaper to implement.
Crystal clear failures. When tests fail, they pinpoint the exact business rule that broke—not some obscure integration issue buried in browser logs or network traces.
BDD at unit level transforms business logic from a slow, complex, system-level concern into a fast, focused, unit-level asset that everyone can understand and verify immediately.
More effective. Lower cost. Simpler to maintain.
Integration tests still matter — but you’ll need far fewer of them. By verifying business rules at the unit level, you’ve already tested about 90% of what matters.
What remains are (simpler) integration risks that require only a handful of scenarios. You don’t need to re-validate every business rule through the full stack — just confirm that the pieces connect correctly:
- Data flows through adapters and infrastructure
- Edge-to-edge connections work (API ↔ logic ↔ database)
- System behavior remains correct when components interact
Think of it this way: If you have 100 business logic scenarios covered at unit level, you might only need 5–10 integration tests to verify the wiring. The bulk of your confidence now comes from fast, focused, low-level tests.
📘 Part 4 of this series will dive deeper into concrete strategies for minimizing integration pain while keeping most tests fast and local.
So you want to try BDD at the unit level? Perfect — start small, and let confidence grow with each step.
The barrier to entry is low. The hardest part isn’t the tooling — it’s the mindset shift.
Here’s a practical, low-risk path to get started 👇
1️⃣ Pick a simple business rule — something familiar yet self-contained. Good candidates include:
- Discount calculation – Apply percentage based on customer tier
- Shipping cost logic – Calculate based on weight and destination
- Tax calculation – Apply rates based on location and product type
2️⃣ Set up a minimal project:
- Use the same language as your production code
- Pick a BDD framework (e.g. Cucumber) - 💡 Note: BDD doesn’t require Cucumber, but I’ll use it as an example in this series
- Create a basic folder structure - ✨ Tip: AI tools like ChatGPT or Claude can scaffold this for you in seconds
3️⃣ Implement and test:
- Extract the core logic into a pure function (“JSON in → JSON out”)
- Write 3–5 concise Gherkin scenarios covering main cases
- Implement the step definitions that call your function
- Run the tests — they should pass in milliseconds ⚡
4️⃣ Share your success with teammates — small wins build momentum.
⏱ Time investment: 1–3 hours 🎯 Goal: See that BDD at unit level works — and feels natural.
Once your playground works, bring it closer to reality.
1️⃣ Pick one feature from your backlog — ideally:
• New (greenfield) rather than refactoring existing code
• Moderate complexity — not trivial, not overwhelming
• Clear business rules — calculations, validations, or workflows
2️⃣ During planning, write Gherkin scenarios with stakeholders
3️⃣ Implement the playground project:
- After planning, create a business logic prototype project for this feature
- Implement the logic and make it executable through BDD tests
- Demo the working prototype to developers and business peers
⏱ Time investment: A few hours to a few days (usually within one sprint)
🎯 Goal: Experience the first tangible advantages of BDD for a real feature
Your prototype works — now it’s time to ship it.
-
Copy the pure functions into your main codebase
-
Copy Gherkin scenarios + step definitions into your test suite
-
Run them in CI/CD as part of your pipeline
-
Build the adapters around your logic:
• API endpoints to parse requests and call functions
• Database adapters for persistence
• UI components to display results
💡 Key insight: Your business logic is already tested and verified — now you’re just wiring it up to the system.
⏱ Time investment: a few days (depending on infrastructure)
🎯 Goal: Experience in production a first example of using this approach
After your first real feature gradually expand it: apply to more features as you build confidence, until you always apply it to most of the business logic.
After your first real feature, expand gradually — apply BDD at unit level to more features as your confidence grows. Over time, it’ll become your natural way of designing and testing business logic.
Then, iterate your approach — refine how you write scenarios, collaborate, and structure your tests so it fits your team’s rhythm.
Once it’s part of your everyday flow, BDD at the unit level feels effortless — just another step in thinking clearly about behavior and intent.
In Part 2, I'll share why I developed this approach and lessons learned from using it with various teams for over a decade.
👉 Want to follow the rest of the series? Follow me on Medium or connect on LinkedIn*