>_ Analyst Engineering

From Use Cases to UAT Scenarios: Main Flow, Alternates, Exceptions, and a Sign-Off Pack

Written by Ahmed at Analyst Engineering, a Senior Technical Business Analyst with 10+ years in banking and payments delivery.

Cover for a guide to writing use cases and generating UAT scenarios in Gherkin with a traceable sign-off pack.

Key takeaways

  • A use case with only a main flow is a wish. The alternate and exception flows are the document, because that is where the system either behaves or does not.
  • Write use cases as structured data with an id per flow and per step, so a UAT scenario can cite the exact step it proves rather than the use case as a whole.
  • Generate UAT scenarios from the use case flows, never from the requirements directly. Requirements produce disconnected checks; flows produce scenarios a business user recognises as their job.
  • Every scenario carries the requirement id as a Gherkin tag. That single tag is what turns the sign-off pack, the traceability matrix, and the coverage gate into generated artifacts rather than maintained ones.
  • UAT is not a second round of system testing. If a UAT scenario fails because a field validation is wrong, the pipeline failed upstream, because that defect should never have reached a business user.

Use cases are the bridge between requirements and UAT. Write them as structured data with an id per flow and per step, generate one Gherkin scenario per flow in business language, tag every scenario with the requirement id it proves, and the sign-off pack becomes a generated artifact rather than a spreadsheet somebody assembles the night before the go-live meeting.

Two failures show up in almost every UAT I have been near. The first is a test pack written from the requirements directly, which produces forty disconnected checks that a business user reads with visible bafflement because none of them resembles anything they do. The second is a use case document with a beautiful main flow, two alternates, and no exception flows at all, which means the entire question of what happens when things go wrong was never asked until it went wrong.

Both come from the same missing artifact: a use case written properly, in a form a scenario can be generated from. This is stage six and seven of the requirements to UAT pipeline. The conceptual ground on UAT itself is in user acceptance testing; this article is the production line. If you want the use case and UAT templates in document form, they are in The BA Deliverables Template Pack.

What does a use case look like as structured data?

Same principle as requirements as code: a record, not prose. The difference is that a use case has flows, and each flow has steps, and each step needs an id so a scenario can cite it.

# requirements/use-cases.yaml
- id: UC-007
  title: Submit a single payment from the merchant portal
  realises: [FR-028, FR-031, FR-033]
  actor: Merchant finance user
  goal: Send a payment to a supplier and know it was accepted.
  preconditions:
    - The user is authenticated with the payments-submit role.
    - The originating account is active and has an available balance.
  trigger: The user selects New payment in the portal.

  main_flow:
    - id: UC-007-M1
      step: The user enters beneficiary, amount, currency, and reference.
    - id: UC-007-M2
      step: The system validates the beneficiary against the saved list.
    - id: UC-007-M3
      step: The system evaluates the daily limit for the currency.
      realises: FR-031
    - id: UC-007-M4
      step: The user confirms the payment summary.
    - id: UC-007-M5
      step: The system accepts the instruction and returns a reference.
    - id: UC-007-M6
      step: The user sees the payment in Sent with status Accepted.
  postconditions:
    - A payment instruction exists in state ACCP with a UETR.
    - The available balance is reduced by the instructed amount.

  alternate_flows:
    - id: UC-007-A1
      branches_at: UC-007-M1
      condition: The beneficiary is not in the saved list.
      steps:
        - The user enters full beneficiary details including IBAN and BIC.
        - The system validates the IBAN checksum and resolves the BIC.
        - Flow resumes at UC-007-M3.
    - id: UC-007-A2
      branches_at: UC-007-M4
      condition: The payment requires dual approval above 10,000 EUR.
      realises: FR-033
      steps:
        - The system places the instruction in state PDNG awaiting approval.
        - A second authorised user approves or rejects it.
        - On approval the flow resumes at UC-007-M5.

  exception_flows:
    - id: UC-007-E1
      branches_at: UC-007-M3
      condition: The payment would breach the daily limit.
      realises: FR-031
      steps:
        - The system rejects the instruction with reason code AM04.
        - The user sees the limit, the amount already used, and the shortfall.
        - No funds are reserved and the available balance is unchanged.
      user_outcome: The payment is not sent and the user knows why.
    - id: UC-007-E2
      branches_at: UC-007-M5
      condition: The sanctions service does not respond within 3 seconds.
      steps:
        - The instruction is held in state PDNG with reason Screening.
        - The user sees Pending screening, not an error.
        - Operations are alerted after 15 minutes in this state.
      user_outcome: The payment is neither lost nor confirmed; it is pending.

Three things in there do real work.

realises at the step and flow level. A functional requirement is usually proven by one step, not by the whole use case. Citing the step means a coverage report can say precisely where FR-031 is exercised.

branches_at. An alternate flow that does not say where it leaves the main flow and where it returns is the most common defect in use case documents, and it produces test scenarios that start from nowhere.

user_outcome on every exception flow. This forces the question that exception flows exist to answer: what does the person actually experience? UC-007-E2 is the interesting one, because “pending” is a genuinely different outcome from “error”, and a specification that does not distinguish them produces a portal that tells a merchant their payment failed when it is being screened.

Generating the alternate and exception flows you would forget

Humans write main flows well and exception flows badly, because writing an exception flow requires you to stop imagining success. A model has no such difficulty.

Here is use case UC-007 with its main flow only, plus the functional
requirements it realises, the API contract, and the gap register.

Walk the main flow step by step. At each step, ask two questions and
answer both from the artifacts:

1. ALTERNATE: what else might this actor reasonably do here, that
   still reaches the goal by a different route?
2. EXCEPTION: what could go wrong here, that prevents the goal?

For each exception you propose, state:
- the condition, precisely
- what the system does
- what the ACTOR SEES, in their words, not a status code
- what state the system is left in
- whether anything was reserved, charged, or sent that must be undone

Rules:
- Cite the requirement or the contract line that supports it. If
  nothing does, mark it NOT SPECIFIED and list it as a question.
- Do not invent business policy. If the artifacts do not say whether
  a held payment expires, say so.
- Include the boring ones: session expiry mid-flow, browser back
  button after confirmation, double submit of the confirm action.

The last instruction earns its place every time. Double submission of a confirm button is the single most common production cause of duplicate payments, it is trivially foreseeable, and it appears in almost no use case document because it feels too small to write down. A model asked explicitly for it writes it in one line.

Run the blind spot lenses over the finished use case as well. The concurrency and lifecycle lenses find different things when applied to a flow than they do when applied to a requirement, because a flow has an order and a duration.

Generating UAT scenarios from flows

Now the transformation that matters. One Gherkin scenario per flow, in the language of the business.

Generate UAT scenarios from the attached use case.

One scenario per flow (main, each alternate, each exception). More
than one only if a flow has genuinely distinct data variants.

Requirements for every scenario:
- Tag it with the use case flow id and every requirement id it proves:
  @UC-007-E1 @REQ-014 @FR-031 @uat
- Write Given/When/Then in the vocabulary a merchant finance user
  uses. Never mention endpoints, status codes, database tables,
  services, or queues.
- Use concrete data. "a payment of 5,000 EUR" not "a valid amount".
  Invent nothing: take values from the test data section if present,
  otherwise mark DATA NEEDED.
- The Then steps must assert a business outcome the user can observe,
  plus any state a business user would check (the balance, the audit
  entry, the notification).
- Every scenario must be executable by someone who does not know how
  the system is built.

Output valid Gherkin only.

The vocabulary constraint is the whole point of generating from flows rather than requirements. Compare:

# Generated from the requirement. This is a system test wearing a UAT badge.
@FR-031
Scenario: Daily limit exceeded returns AM04
  Given customer NP-4471 has dailyLimitAmount 50000.00 EUR
  When POST /payments is called with instructedAmount 5000.00
  Then the response status is 422
  And the response body contains reasonCode "AM04"
# Generated from the exception flow. A business user can run this.
@UC-007-E1 @REQ-014 @FR-031 @uat @priority-critical
Scenario: A payment that would breach the daily limit is refused
  Given I am signed in to the merchant portal as a finance user
  And my daily payment limit is 50,000 EUR
  And I have already sent 48,000 EUR today
  When I submit a payment of 5,000 EUR to supplier Delacroix SARL
  Then the payment is not sent
  And I am told that it would exceed my daily limit
  And I can see that 48,000 EUR of my 50,000 EUR limit is used
  And my available balance is unchanged
  And the attempt appears in my activity history

Both prove the same rule. Only the second one tells you whether the feature is usable, and only the second one gets a meaningful pass or fail from a person who does the job. The two are complements, not alternatives: the first belongs in the API suite covered in API test cases, the second in UAT.

Note also what the second scenario caught. “I can see that 48,000 of my 50,000 limit is used” is not in any requirement. Writing the scenario from the user’s flow surfaced a display requirement nobody had specified, which is a normal and welcome side effect of this stage.

Validating the scenario set before anyone runs it

Three mechanical checks, all in CI.

# scripts/uat-check.py
import re, sys, yaml, glob, pathlib

ucs = yaml.safe_load(open("requirements/use-cases.yaml", encoding="utf-8"))
flow_ids = set()
for uc in ucs:
    flow_ids.add(uc["id"])
    for key in ("main_flow",):
        flow_ids.update(s["id"] for s in uc.get(key, []))
    for key in ("alternate_flows", "exception_flows"):
        flow_ids.update(f["id"] for f in uc.get(key, []))

SYSTEM_WORDS = re.compile(
    r"\b(endpoint|status code|HTTP|payload|JSON|database|table|column|"
    r"queue|topic|API|POST|GET|PUT|DELETE|2\d\d|4\d\d|5\d\d)\b")

errors, covered = [], set()
for path in glob.glob("tests/features/**/*.feature", recursive=True):
    text = pathlib.Path(path).read_text(encoding="utf-8")
    for block in re.split(r"\n(?=\s*@)", text):
        if "Scenario" not in block:
            continue
        tags = set(re.findall(r"@([\w-]+)", block))
        name = re.search(r"Scenario:?\s*(.+)", block).group(1).strip()

        # 1. every UAT scenario cites a flow that exists
        if "uat" in tags:
            flows = tags & flow_ids
            if not flows:
                errors.append(f"{path}: '{name}' cites no known use case flow")
            covered |= flows

            # 2. no system vocabulary in a UAT scenario
            for hit in set(SYSTEM_WORDS.findall(block)):
                errors.append(f"{path}: '{name}' uses system term '{hit}'")

            # 3. no placeholder data
            if "DATA NEEDED" in block or re.search(r"<\w+>", block):
                errors.append(f"{path}: '{name}' has unresolved test data")

# 4. every exception flow has a scenario. This is the one that fails most.
for uc in ucs:
    for f in uc.get("exception_flows", []):
        if f["id"] not in covered:
            errors.append(f'{uc["id"]}: exception flow {f["id"]} has no UAT scenario')

for e in errors:
    print("FAIL", e)
sys.exit(1 if errors else 0)

Check four is the one that changes outcomes. Exception flows are written and then not tested, because the test pack gets built under time pressure and the happy paths go in first. A build that fails on an untested exception flow removes that option.

Check two, the system vocabulary ban, gets argued about. Keep it. The moment 422 appears in a UAT scenario, a business user stops reading and the test becomes something the analyst executes on their behalf, which defeats the purpose of UAT entirely.

The sign-off pack, generated

The go-live meeting needs a document. Generate it from the run rather than assembling it, and it is correct at the moment of the last test execution rather than the moment somebody last updated a slide.

Six sections, all derived:

  1. Scope. Every requirement id in the release, from business.yaml filtered by the release label. Includes what is explicitly out of scope, which is the section that prevents the meeting going sideways.
  2. Traceability matrix. Requirement, functional requirement, use case flow, scenario, last result, run date, commit. One join across the files and the JUnit XML.
  3. Execution summary. Pass, fail, blocked, and not run, grouped by requirement rather than by test, because a stakeholder cares that REQ-014 is proven, not that 37 of 40 tests passed.
  4. Open defects. Severity, the requirement affected, and the business impact in one sentence. Pulled from Jira by the requirement label, using the scoped token setup.
  5. Accepted risks. Every gap with status: accepted-risk from the gap register, with the named owner and the date they accepted it. This section exists so nobody can say later that they were not told.
  6. Sign-off block. Role, name, date, and the commit SHA of the requirements at the moment of signing. That SHA is what lets you answer, in a year, exactly which version of the specification was signed.

Section five is the one teams omit and later wish they had. A go-live decision made in full knowledge of eleven accepted risks is a decision. The same decision made without that list is an accident that happened to work.

The go or no-go decision is the wider framework for what happens in that meeting.

What UAT is for, and what it is not

Worth restating because most UAT packs violate it.

UAT provesUAT does not prove
A business user can complete their taskThat field validation works
The outcome matches what the business meantThat the API returns the right status code
The exception is understandable to the person who hits itThat the retry logic is correct
The process works end to end across teamsThat performance holds under load

If a UAT scenario fails because a mandatory field was not enforced, the failure is upstream: that defect should have been caught in system testing and never reached a business user. Track how many UAT failures are of that kind. A high number is not a UAT problem, it is a measurement of how weak the earlier stages are, and it is the most useful metric this pipeline produces. Where each level of testing belongs is in smoke, sanity, and regression testing.

The takeaway

Use cases are what make UAT scenarios recognisable to the people who have to sign them. Write them as structured data with an id on every flow and every step, use a model to generate the alternate and exception flows you would otherwise skip, then generate one Gherkin scenario per flow in the vocabulary of the business, tagged with the requirement id it proves. Let CI fail the build when an exception flow has no scenario or a scenario mentions a status code.

Then the sign-off pack is generated from the run, complete with the accepted risks and the commit SHA of the specification that was signed. Next, the strategy and the execution that produce those results: test strategy to execution. For the use case and UAT templates, see The BA Deliverables Template Pack, and for the testing side, API Testing and QA Mastery for BAs.

Ahmed is a Senior Technical Business Analyst with 10+ years in banking and payments. He builds practical guides and tools for analysts at The Tech BA Toolkit.

Tags: Functional Analysis, UAT, Use Cases, Gherkin, Requirements

About the author

Analyst Engineering is written by Ahmed, a Senior Technical Business Analyst with 10+ years of banking and payments delivery experience: ISO 20022 and SWIFT messaging, payments API integration, Kafka event validation, and production support. Every article comes from real delivery work, and each one is reviewed and updated as tools and standards change.

Free account

Practice on the Labs, keep your progress

A free account, no password: an email link signs you in. It saves your steps and self-assessments on the Labs, shows your missions on a dashboard, unlocks the solutions, and, if you tick the box, sends you new missions and articles when they ship.

Your email is used to sign you in. Nothing else, unless you ask. Privacy.