ATDD Guide: Acceptance Test-Driven Development for Teams
ATDD (Acceptance Test-Driven Development) starts each feature with a failing acceptance test written in collaboration between developers, testers, and product owners. Unlike TDD (which is a solo technical practice) and BDD (which focuses on communication and specification language), ATDD is specifically about driving development from acceptance criteria. The cycle is discuss → distill → develop → demo. The toolchain is Cucumber + JUnit 5 + Spring Boot.
Three acronyms — TDD, BDD, ATDD — describe related but distinct practices. Teams conflate them, argue about which they are "doing," and miss the practical point of each. Getting the distinctions clear is not academic. It changes who is in the room, what gets written, and when.
TDD vs BDD vs ATDD
TDD (Test-Driven Development) is a developer's practice. It operates at the unit level: write a failing test for a small piece of code, write the minimum code to make it pass, refactor. The feedback loop is seconds. The audience is the developer. TDD is about design — it forces you to think about interfaces before implementations.
BDD (Behavior-Driven Development) is a communication practice. It uses a shared language (Given/When/Then, Gherkin) to describe system behavior in terms that both technical and non-technical stakeholders can read and verify. BDD is about collaboration — it ensures that the team is building the right thing by making the specification readable to everyone.
ATDD (Acceptance Test-Driven Development) is a delivery practice. It extends TDD outward: instead of starting with a unit test, you start with an acceptance test that captures the complete behavior expected from a feature. ATDD is about completeness — no feature is done until it passes its acceptance test.
The relationship is layered:
ATDD — acceptance tests drive the feature
BDD — Gherkin makes those acceptance tests human-readable
TDD — unit tests drive the implementation inside each featureYou can do TDD without BDD or ATDD. You can do BDD without doing ATDD (writing scenarios after implementation — a common and mostly useless pattern). ATDD done well uses BDD-style scenarios as acceptance tests and TDD for the internal implementation.
The ATDD Cycle: Discuss → Distill → Develop → Demo
Discuss
Before writing any test or code, the team — developer, tester, product owner, sometimes a UX designer — has a structured conversation about the feature. This is not a sprint planning where the product owner presents requirements and the team estimates. It is a three-way conversation where everyone contributes examples.
The product owner explains the business need. The developer asks technical constraint questions. The tester asks "what about" questions: what about an empty input? What about a user without the right permission? What about a network timeout during the operation?
Example Mapping (covered in a separate post) is a structured format for this conversation. The output is a set of concrete examples that everyone agrees represent the feature's complete behavior.
Who participates: developer + tester + product owner. All three. Missing any one produces a blind spot.
Output: a set of concrete examples, documented as sticky notes, Gherkin scenarios, or structured notes.
Time: 25–45 minutes per feature, before any implementation starts.
Distill
The concrete examples from the discussion are distilled into formal acceptance tests. In practice, this means writing Gherkin feature files.
Distillation is a collaborative act. The tester often drives, but the product owner reviews each scenario for correctness and the developer reviews for technical completeness. A scenario that the product owner cannot understand is not yet a good scenario.
Feature: Customer Account Suspension
As a billing administrator
I want to suspend customer accounts that have overdue invoices
So that we stop providing service to customers who aren't paying
Background:
Given the billing system is operational
And I am logged in as a billing administrator
Scenario: Suspending an account with overdue invoices immediately blocks new orders
Given customer "Acme Corp" has an invoice that is 30 days overdue
When I suspend the "Acme Corp" account
Then the account status is "Suspended"
And new orders from "Acme Corp" are rejected with "Account suspended due to overdue balance"
And existing pending orders are placed on hold
Scenario: Suspending an account sends a notification to the customer
Given customer "Acme Corp" has an invoice that is 30 days overdue
When I suspend the "Acme Corp" account
Then an email notification is sent to the primary contact of "Acme Corp"
And the notification explains the suspension reason and payment instructions
Scenario: Accounts with no overdue invoices cannot be suspended
Given customer "Globocorp" has no overdue invoices
When I attempt to suspend the "Globocorp" account
Then the suspension is rejected with "No overdue invoices found"
And the "Globocorp" account status remains "Active"These scenarios are committed to the repository before implementation begins. They represent the acceptance criteria. They will fail — that is correct and expected.
Develop
With failing acceptance tests in place, development begins. The developer writes implementation code to make the scenarios pass, using TDD for the internal layers.
The acceptance test is the outermost boundary. Inside it, the developer writes unit tests for business logic, repository tests for data access, integration tests for external calls. The acceptance test does not replace these — it bounds them.
// Step definition — connects Gherkin to implementation
public class AccountSuspensionSteps {
@Autowired
private AccountService accountService;
@Autowired
private OrderService orderService;
@Autowired
private EmailCapture emailCapture;
private String targetCustomer;
private Exception lastException;
@When("I suspend the {string} account")
public void suspendAccount(String customerName) {
try {
accountService.suspend(customerName, "Overdue invoice");
} catch (Exception e) {
lastException = e;
}
}
@Then("the account status is {string}")
public void verifyAccountStatus(String expectedStatus) {
var account = accountService.findByName(targetCustomer);
assertThat(account.status().name()).isEqualTo(expectedStatus);
}
@Then("new orders from {string} are rejected with {string}")
public void verifyOrdersRejected(String customerName, String expectedMessage) {
var exception = assertThrows(
OrderRejectionException.class,
() -> orderService.createOrder(customerName, List.of(new OrderItem("SKU-001", 1, BigDecimal.TEN)))
);
assertThat(exception.getMessage()).isEqualTo(expectedMessage);
}
@Then("an email notification is sent to the primary contact of {string}")
public void verifyEmailSent(String customerName) {
var emails = emailCapture.sentTo(customerName);
assertThat(emails).hasSize(1);
}
@Then("the suspension is rejected with {string}")
public void verifySuspensionRejected(String expectedMessage) {
assertThat(lastException)
.isInstanceOf(SuspensionNotAllowedException.class)
.hasMessage(expectedMessage);
}
}The developer runs ./gradlew test repeatedly. Scenarios move from red to green as implementation progresses. The acceptance tests are the spec — the developer is done when they are all green, not when the code "looks right."
Demo
The demo is the final step of the ATDD cycle. With all acceptance tests green, the developer demonstrates the feature to the product owner — ideally showing the Serenity BDD or Cucumber report alongside the working application.
The demo serves two purposes:
- Verification: the product owner confirms that the passing scenarios represent the behavior they expected
- Discovery: the product owner often sees the working feature and thinks of edge cases that were not discussed
New edge cases from the demo become new scenarios. If they represent small clarifications, they go into the current story. If they represent significant new scope, they become new stories for future sprints.
The ATDD cycle then repeats: discuss the new scenario, distill it into Gherkin, develop until it passes, demo again.
Tool Chain: Cucumber + JUnit 5 + Spring Boot
Project structure:
src/
main/java/com/example/
account/
AccountService.java
AccountRepository.java
Account.java
test/java/com/example/
acceptance/
AccountSuspensionSteps.java
OrderRejectionSteps.java
unit/
AccountServiceTest.java
SuspensionPolicyTest.java
CucumberTestSuite.java
test/resources/
features/
account-suspension.feature
order-placement.featureJUnit 5 + Cucumber integration:
@Suite
@IncludeEngines("cucumber")
@SelectClasspathResource("features")
@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME,
value = "pretty,html:build/reports/cucumber.html,json:build/reports/cucumber.json")
@ConfigurationParameter(key = GLUE_PROPERTY_NAME,
value = "com.example.acceptance")
public class CucumberTestSuite {
}Spring Boot test context for acceptance tests:
@CucumberContextConfiguration
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase
@Transactional
public class CucumberSpringConfiguration {
@Autowired
private AccountRepository accountRepository;
@Before
public void setUp() {
// Reset state between scenarios
accountRepository.deleteAll();
}
}The @Transactional on the Spring configuration class rolls back after each scenario by default in Spring. For acceptance tests that test side effects (like emails sent), you may want explicit cleanup instead of transaction rollback.
Team Workflow: Who Writes What and When
ATDD changes the team's workflow in concrete ways. Here is a sprint-level picture:
Sprint planning (Monday):
- Product owner presents feature requests with business context
- Team runs Example Mapping for the top 3–5 stories
- Stories with unresolved red cards are not committed to the sprint
Acceptance test authoring (Monday afternoon):
- Tester writes Gherkin feature files based on the Example Mapping output
- Developer reviews for technical completeness
- Product owner reviews for business correctness
- Feature files are committed — all scenarios are red
Implementation (Tuesday–Thursday):
- Developer picks the first failing scenario
- Writes unit tests, implements, makes the scenario pass
- Moves to the next failing scenario
- Tester adds scenarios if exploratory testing reveals missing cases
Demo and sign-off (Friday):
- Developer demos the feature with acceptance report
- Product owner confirms scenarios cover the expected behavior
- Any new scenarios discovered are logged for the next sprint
Definition of Done includes:
- All acceptance scenarios passing in CI
- Serenity report published and linked in the story ticket
- Code reviewed
- No new red cards unresolved
Common Failure Modes
Writing acceptance tests after implementation. This is the most common anti-pattern. It feels like ATDD but misses the point — the tests do not drive design, they document what was built. The code shapes the test rather than the other way around.
Treating acceptance tests as the only tests. ATDD acceptance tests operate at the behavior level. They do not replace unit tests for business logic or integration tests for data access. A pyramid still applies — acceptance tests are the top layer, not the only layer.
Product owner disengagement. If the product owner stops reviewing scenarios, ATDD degrades into developers writing tests for requirements they interpreted themselves. The three-amigos collaboration is not optional — it is the mechanism that makes ATDD produce value.
Scenarios that test implementation, not behavior. "Given the suspension flag in the database is set to true" is not a behavioral scenario — it is a database state description. "Given customer Acme Corp has an overdue invoice" is behavioral. Keep scenarios at the domain level.
Conclusion
ATDD is the practice that bridges planning and implementation. The discuss phase ensures everyone agrees on what to build. The distill phase makes that agreement explicit and executable. The develop phase uses failing tests as a guide. The demo phase closes the loop with the people who defined the requirement.
Teams that adopt ATDD report fewer "it's done but not what we wanted" moments in sprint reviews. The mechanism is simple: when the acceptance test is written before the implementation, there is no ambiguity about what "done" means. Done means the acceptance tests pass. That is it.