Acceptance criteria testers can automate
Gherkin for payment flows
Gherkin for payment flows
Most acceptance criteria are written to be read, not executed. "The payment should be processed correctly." "An error is shown if the limit is exceeded." "Transfers after the cut-off go the next day." A tester reading those lines still has to ask the BA what "correctly" means, which error, which limit and which cut-off. The answers end up in a test script the BA never sees, so requirements and tests drift apart from the first sprint.
In payment flows, that gap is expensive. The defects that reach production are rarely in the main path. They sit at the boundaries: an amount exactly at the limit, a transfer submitted at the cut-off minute, a Friday afternoon before a bank holiday. This article, written jointly from the BA and tester side, shows how to write acceptance criteria in Gherkin that are precise enough to become automated tests without rewriting, using Cucumber-JVM with Spring Boot.
Here is a typical criterion for a daily transfer limit:
Scenario: Limit exceeded
Given a user with insufficient limit
When they make a large payment
Then an error should be shown
It reads well, but it can't be automated as written, and it leaves the important decisions open:
"Insufficient limit" says nothing about the limit or how much has already been used.
"A large payment" isn't a value. Is the limit inclusive? Does a payment of exactly the remaining amount pass?
"An error" doesn't say which message, or whether the payment is rejected, held or sent for approval.
The executable version makes every one of those decisions explicit:
@PAY-LIMIT-001
Feature: Daily transfer limit for retail customers
Background:
Given the customer has a daily transfer limit of 5000.00 EUR
Scenario Outline: Transfers are checked against the remaining daily limit
Given the customer has already transferred <already> EUR today
When the customer submits a SEPA transfer of <amount> EUR
Then the transfer is <outcome>
Examples:
| already | amount | outcome |
| 0.00 | 5000.00 | accepted |
| 0.00 | 5000.01 | rejected |
| 4999.99 | 0.01 | accepted |
| 4999.99 | 0.02 | rejected |
Scenario: The customer is told why a transfer over the limit was rejected
Given the customer has already transferred 4500.00 EUR today
When the customer submits a SEPA transfer of 600.00 EUR
Then the transfer is rejected
And the customer sees the message "payment.limit.daily.exceeded"
And no amount is reserved on the customer's account
The BA can review it with the product owner: the limit is inclusive, the boundary is tested on both sides, and a rejected transfer reserves nothing. The tester can automate it directly. And the next time someone asks how the daily limit works, the feature file is the answer.
These are the rules we apply when a BA and a tester review criteria together.
Every amount, date, time, currency and status is a real value. "A large amount" becomes 5000.01 EUR. If the business hasn't decided the value yet, that's a question to resolve before the story is ready, not something for the tester to guess.
Payment rules are full of thresholds: limits, cut-off times, minimum amounts, maximum lengths. For each one, specify the value just inside, exactly at, and just outside. A Scenario Outline with an Examples block expresses this compactly, as in the limit example above.
Steps like "click the Send button" or "enter the IBAN in field 3" tie the criteria to one UI and break whenever the design changes. Write steps in business language ("the customer submits a SEPA transfer") and let the step definitions decide whether that means a UI action, an API call or a service call. The same feature can then run against the API in CI and against the app in an end-to-end suite.
Payment behaviour often depends on the date and time: cut-off times, business days, holidays and value dates. Criteria that rely on "today" pass on Tuesday and fail on Saturday. State the date, the day type, the time and the time zone in the scenario:
@PAY-CUTOFF-002
Feature: Execution date for standard SEPA transfers
Rule: Transfers submitted at or after 16:00 are executed on the next business day
Scenario: Submitted one minute before the cut-off
Given today is Wednesday 4 November 2026, a business day
And the time is 15:59 in Europe/Bucharest
When the customer submits a standard SEPA transfer
Then the requested execution date is 2026-11-04
Scenario: Submitted at the cut-off on a Friday
Given today is Friday 6 November 2026, a business day
And the time is 16:00 in Europe/Bucharest
When the customer submits a standard SEPA transfer
Then the requested execution date is 2026-11-09
The Rule keyword, available since Gherkin 6, groups scenarios under the business rule they illustrate. Writing "at or after 16:00" in the rule forced a decision that the original criterion ("after the cut-off") left open: is 16:00 itself too late?
A Then step checks something a customer, an operator or another system can observe: the payment status, the message shown, the execution date, the amount reserved, the event published. It doesn't check internal method calls or database columns that exist only for implementation reasons. Observable outcomes keep the tests valid when the code is refactored.
A scenario with three When steps and six Then steps is several tests in one. When it fails, nobody can tell which rule broke. Split it, so that each scenario's name states the rule it proves.
"The customer sees the message 'payment.limit.daily.exceeded'" stays stable when the wording changes or the app is translated. The wording itself lives in a content register reviewed by the business and legal teams.
With criteria written this way, the step definitions are short. Cucumber runs inside a Spring Boot test context, so steps can use real services, a test database and a controllable clock:
1 @CucumberContextConfiguration
2 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
3 @Import(TestClockConfiguration.class)
4 public class CucumberSpringConfiguration {
5 }
6 public class TransferLimitSteps {
7
8 @Autowired private TestCustomers customers;
9 @Autowired private PaymentsApiClient payments;
10
11 private PaymentResult result;
12
13 @Given("the customer has a daily transfer limit of {bigdecimal} EUR")
14 public void dailyLimit(BigDecimal limit) {
15 customers.current().setDailyLimit(limit);
16 }
17
18 @Given("the customer has already transferred {bigdecimal} EUR today")
19 public void alreadyTransferred(BigDecimal amount) {
20 customers.current().recordTransfersToday(amount);
21 }
22
23 @When("the customer submits a SEPA transfer of {bigdecimal} EUR")
24 public void submitTransfer(BigDecimal amount) {
25 result = payments.submitSepa(customers.current(), amount);
26 }
27
28 @Then("the transfer is {word}")
29 public void transferOutcome(String expected) {
30 assertThat(result.status().name()).isEqualToIgnoringCase(expected);
31 }
32
33 @Then("the customer sees the message {string}")
34 public void messageShown(String messageKey) {
35 assertThat(result.messageKey()).isEqualTo(messageKey);
36 }
37 }
Three details make this work in a payment context:
{bigdecimal} for amounts. Cucumber's built-in parameter type parses amounts as BigDecimal, so money never passes through a floating-point type.
A controllable clock. The application uses an injected java.time.Clock. In tests, TestClockConfiguration provides one the cut-off steps can set to a fixed instant and time zone, so "15:59 in Europe/Bucharest" means exactly that on any build server.
Tags for traceability. @PAY-LIMIT-001 links the feature to the requirement, so test reports show which rules passed and which failed. Tags also let the pipeline run subsets, such as every scenario for a regulatory requirement before a release.
Precise criteria don't come from the BA alone or from the tester alone. The format that works best for us is a short session per story, often called a "three amigos" conversation, with the BA, a tester and a developer. Example Mapping is a useful structure for it: the story at the top, its rules underneath, concrete examples under each rule and open questions on the side.
The roles are complementary:
The BA owns the rules and their meaning, and brings the business and regulatory context: why the limit exists, who can change it, what the customer should see.
The tester looks for the boundaries and the unhappy paths: exactly at the limit, one minute before the cut-off, the Friday before a holiday, a second submission of the same payment.
The developer checks that each example can be set up and observed, and points out where a step would need hidden internals.
A story is ready when every rule has examples with concrete values and there are no open questions left. The feature file written in that session becomes both the acceptance criteria in the backlog and the automated test in the repository.
Hiding important values in step definitions. If the limit is set in Java code rather than in the scenario, the business can't see or review it.
Overly generic steps. A step like the {string} field is {string} makes scenarios read like form-filling instructions and hides the business meaning.
Incidental detail. Customer names, addresses and references that don't affect the outcome make scenarios longer without making them more precise. Keep only the data that matters to the rule.
Writing scenarios after the code. Criteria written to describe what was built test the implementation, not the requirement. The value comes from agreeing the examples first.
Acceptance criteria are the most important place where the business and delivery teams agree on what "done" means. Written in Gherkin with concrete values, explicit boundaries, explicit time and observable outcomes, they stop being a summary that testers have to interpret and become the test itself. For payment flows, where defects live at the edges, that precision is what turns acceptance criteria into the bank's first line of defence against releasing the wrong behaviour.