Cucumber Framework

The TypeScript/Cucumber BDD framework drives end-to-end integration tests against the FloatMe QA environment. Each scenario assembles a synthetic user through a declarative Given/When/Then pipeline, calls real service APIs over SigV4-signed HTTP, and asserts on the responses.

cucumber test flow

Folder Layout

cucumber/
├── features/
│   ├── features/          ← .feature files (imported from Jira/Xray)
│   └── step-definitions/  ← TypeScript step implementations
├── import_features.sh     ← pulls feature files from Xray
├── export_results.sh      ← pushes results back to Xray
├── cucumber.json          ← runner config (parallel, retry, loader)
└── .env                   ← local env overrides (not committed)

World Context

customWorld extends Cucumber’s World class and carries all state that is shared across steps within a single scenario. Every scenario starts with a fresh customWorld instance; nothing persists between scenarios.

Field Type Populated By Purpose

userID

string

First When step (The user is created)

Returned by POST /qa/integration/custom-user; threaded through all subsequent API calls as the path parameter.

loanID

string

Given (pre-seeded loan) or LOC When step (The user creates a loan …)

Identifies the active LOC loan for payment, collection, and assertion steps.

loanApplicationID

string

When (The user starts a loan application …)

Returned by POST /{userID}/loans/applications; required to generate and confirm loan details.

confirmationID

string

When (The user makes a manual payment …)

Returned by POST /{userID}/loans/{loanID}/payback; used when sending a failed-payment event to Kinesis.

paymentAmount

number

When (The user makes a manual payment …)

Stored alongside confirmationID so the Kinesis event step can echo the correct amount back.

aws

AwsClient

Before hook (per-scenario)

aws4fetch client pre-loaded with temporary STS credentials; all service calls use this.aws.fetch() for SigV4 signing.

qaApiUrl

string

Before hook (per-scenario)

Base URL for the QA simulation API (/qa/… and /qa/simulate/… endpoints).

subscriptionsApiUrl

string

Before hook (per-scenario)

Base URL for the Subscription service.

underwritingApiUrl

string

Before hook (per-scenario)

Base URL for the Underwriting service.

locApiUrl

string

Before hook (per-scenario)

Base URL for the Line of Credit service.

paymentsApiUrl

string

Before hook (per-scenario)

Base URL for the Payments service.

userApiUrl

string

Before hook (per-scenario)

Base URL for the User service.

floatApiUrl

string

Before hook (per-scenario)

Base URL for the Float service.

customUser

QAIntegrationUser

Given steps (builder pattern)

Accumulates the full user specification sent to POST /qa/integration/custom-user. Built incrementally before user creation.

cachedSubscriptions

Subscription[]

Then assertion steps in subscriptions.ts

Lazy-loaded from GET /{userID}/subscriptions; reused across multiple Then steps in the same scenario.

cachedMembership

Membership

When (The user’s membership is retrieved) or Then steps in membership.ts

Lazy-loaded from GET /{userID}/user/membership.

cachedUser

User

When (The user is retrieved) or Then steps in membership.ts

Lazy-loaded from GET /users/{userID}.

cachedLoans

LocLoan[]

Then assertion steps and collection When steps in loc.ts

Lazy-loaded from GET /{userID}/loans; explicitly cleared (= undefined) after mutations to force a fresh fetch.

cachedUnderwritingResults

UnderwritingEligibility

Then assertion steps in underwriting.ts

Lazy-loaded from GET /{userID}/underwriting/eligibility.

cachedPayments

Payment[]

When (The users payments are retrieved) or Then steps in payments.ts

Lazy-loaded from GET /{userID}/payments.

cachedFloats

Float[]

When / Then steps in floats.ts

Lazy-loaded from GET /{userID}/floats.

cachedActiveFloats

Float[]

When / Then steps in floats.ts

Lazy-loaded from GET /{userID}/floats/active.

cachedFloatHistory

FloatHistory[]

When (The {string} floats history is retrieved)

Loaded from GET /{userID}/floats/{floatID}/collection-history.

loanDetailsResponse

LoanDetailsResponse

When (The user generates loan details …)

Holds the proposed installment schedule returned by POST /{userID}/loans/generate_loan_details; referenced by the loan creation step.

lastResponse

Response | undefined

When steps that intentionally allow failure (e.g. float requests)

Raw fetch Response stored for HTTP status code assertion steps (Then The user should receive a {int} status code).

originalInstallmentAmount

number | undefined

When (The loan {int} installment date is updated to be {int} days overdue)

Captures the installment total before a late fee is assessed; used to verify the exact 5% fee calculation.

originalLoanAmount

number | undefined

When (installment backdating step, first call)

Captures the loan total before any late fees are added; used by Then The loan total amount should include late fee.

partialPaymentRemainingBalance

number | undefined

When (The user makes a partial payment …)

Remaining installment balance after a partial payment; available for downstream late-fee calculations.

partialPaymentAmount

number | undefined

When (The user makes a partial payment …)

Amount of the most recent partial payment; available for downstream assertions.

Hooks

BeforeAll

Runs once before the entire test suite. Uses the AWS SDK AssumeRoleCommand to obtain temporary credentials:

  • Role ARN: arn:aws:iam::267052520423:role/test-postman-qa-role

  • Duration: 900 seconds (15 minutes)

The resulting AccessKeyId, SecretAccessKey, and SessionToken are stored on this.parameters so they are available to every scenario’s Before hook.

Before (per-scenario)

Runs before each individual scenario. Reads the temporary credentials from this.parameters and constructs an AwsClient (from aws4fetch) with retries: 0.

All seven API base URLs are assigned as literal execute-api URLs on this:

World Field Hardcoded URL

qaApiUrl

https://sry7g7am12.execute-api.us-west-2.amazonaws.com

paymentsApiUrl

https://h515jjw8ug.execute-api.us-west-2.amazonaws.com

subscriptionsApiUrl

https://wiirdud8ok.execute-api.us-west-2.amazonaws.com

underwritingApiUrl

https://kkpuoihd8c.execute-api.us-west-2.amazonaws.com

locApiUrl

https://uumhh0a38g.execute-api.us-west-2.amazonaws.com

userApiUrl

https://7oweo0qx8a.execute-api.us-west-2.amazonaws.com

floatApiUrl

https://gqdgnz6sy2.execute-api.us-west-2.amazonaws.com

The default step timeout is 60 seconds (setDefaultTimeout(60 * 1000)).

Step Definition Files

File Responsibility Key Steps (representative patterns)

world.ts

Defines the customWorld class with all scenario-level state fields and types.

(no step registrations — world definition only)

hooks.ts

Lifecycle hooks: BeforeAll STS credential exchange, Before per-scenario world initialisation.

(no step registrations — hooks only)

types.ts

TypeScript interfaces for all domain objects (QAIntegrationUser, LocLoan, Installment, Payment, Subscription, etc.) and the companies constant array.

(no step registrations — type declarations only)

helpers.ts

Pure utility functions shared across step files: date formatting, relative-date parsing, ordinal-to-index conversion, random string/account generation.

toISODate(), getDateFromRelative(), getPosition(), getRandomAccountNumber()

user.ts

Given steps that build customUser; the primary When step that creates the user via the QA API. Exports createUser() reused by other files.

Given A user
Given They have a balance of {int}
When The user is created

floats.ts

When steps that request floats or trigger float collections; Then steps that assert float count, status, amount, fee, type, and collection history.

When The user requests a(n) {string} ${int} float
When Float collections are run
Then The {string} float status should be {string}

subscriptions.ts

When steps that trigger subscription collections or pay-ahead; Then steps that assert subscription count, status, error, and amount.

When Subscription collections are run
When The user pays their subscription ahead
Then The {string} subscription status should be {string}

loc.ts

Given steps for pre-seeded loans; When steps for the full LOC application flow, manual payments, partial/full payments, installment date manipulation, and collections triggers; Then steps for loan state, installment status, late fees, and Iterable events.

When The user applies for a loan of {int} with autopay {string} and late fees {string}
When The LOC collections process {word} is triggered with {int} look ahead days
Then The {int} installment should have late fee assessed

membership.ts

When steps for all membership lifecycle actions (upgrade, downgrade, cancel, reactivate, pause, unpause, retract); Then steps that assert user status, membership tier, and membership status.

When The user upgrades to {string} membership
When The user cancels their membership
Then The users membership tier should be {string}

underwriting.ts

When step to trigger an underwriting recalculation; Then step to assert float approval.

When Underwriting runs for the user
Then The user should be approved to float

payments.ts

When steps to retrieve payments or add a debit card via the Payments API; Then steps to assert payment count, status, and amount by payment type (subscription, float, proration).

When The users payments are retrieved
Given The user adds a new debit card
Then The user should have {int} {string} payment(s)

Given / When / Then Conventions

Given Steps — customUser Builder

Every Given step mutates this.customUser — the QAIntegrationUser object that will be POSTed to the QA API. The first Given step (Given A user) initialises the object with faker-generated defaults; all subsequent Given steps add or override specific fields. No network calls are made during Given steps.

Example: building a user with income and a Plaid account
Given A user
And They have a connected plaid account
And They have valid income
And They have a valid debit card
And They have a "standard" "PENDING" float due "5 days from now" for amount 50 and fee 5

The accumulated customUser payload includes nested objects for user, plaid.override_accounts, floats, subscriptions, loans, income, and debit_card.

When Steps — First When Creates the User

The first When step in nearly every scenario calls createUser(), which POSTs the fully assembled customUser to POST /qa/integration/custom-user. The response returns a user_id stored on this.userID. A 10-second sleep follows to allow downstream async processing to complete.

When The user is created

Subsequent When steps call simulation or service endpoints using the now-populated this.userID:

  • POST /qa/simulate/subscriptions/daily — triggers the subscription collections batch for the user

  • POST /qa/simulate/floats/daily — triggers the float collections batch

  • POST /qa/simulate/loc/collections — triggers LOC collections (with process and look_ahead_days params)

  • POST /{userID}/loans/applicationsPOST /{userID}/loans/generate_loan_detailsPOST /{userID}/loans — three-step LOC application flow

  • POST /{userID}/loans/{loanID}/payback — manual loan payment

  • POST /{userID}/user/membership/upgrade|downgrade|cancel|pause|unpause — membership lifecycle actions

Then Steps — Assertion Patterns

Then steps call service read APIs directly with this.aws.fetch() (SigV4-signed) and use Chai expect or assert for assertions. Responses are cached on the world object using the nullish-assignment pattern (??=) so multiple Then steps in the same scenario share one network round-trip.

Example: asserting float state after collections
Then The user should have 1 float(s)
And The "first" float status should be "COMPLETED"
And The "first" float amount should be 50

Error-path assertions use this.lastResponse (stored by steps that intentionally suppress throw):

Then The user should receive a 400 status code

Parallelism and Retry

Configuration lives in cucumber/cucumber.json:

{
    "default": {
        "loader": ["ts-node/esm"],
        "import": ["features/step-definitions/**/*.ts"],
        "parallel": 5,
        "retry": 1,
        "strict": false
    }
}
  • parallel: 5 — up to 5 scenarios run concurrently using Cucumber’s worker pool. Each worker gets its own customWorld instance and its own Before hook invocation (separate AwsClient).

  • retry: 1 — a failing scenario is automatically retried once before being marked failed. This tolerates transient API latency or async processing delays.

  • strict: false — undefined or pending steps are reported but do not abort the run.

The test runner is invoked with:

yarn test   # expands to: cucumber-js -f json:out.json

Results are written to out.json for Xray import.

Tagging Conventions

Each .feature file carries a feature-level tag matching its Jira issue key (e.g. @ENG-8820). Individual scenarios may carry additional tags for grouping or filtering.

  • import_features.sh — pulls updated .feature files from Xray using the issue keys embedded in file names.

  • export_results.sh — reads out.json after a run and pushes results back to Xray, matching results to test executions by tag.

Running a single suite by tag:

cucumber-js --tags @ENG-9692

Test Suites

Feature File Focus Area Jira Key

ENG-8820_Subscription_Collections.feature

Subscription billing: COMPLETED, FAILED, RETRIED, error codes, pay-ahead flows

ENG-8820

ENG-9250_Underwriting_Cucumber.feature

Underwriting eligibility: income detection, balance thresholds, float approval/denial

ENG-9250

ENG-9692_LOC_Cucumber_Test_Set.feature

Line of Credit: loan origination, collections, manual payments, late fees, Iterable events

ENG-9692

ENG-11544_Users_and_Membership.feature

User lifecycle and membership tier management: upgrade, downgrade, cancel, pause, reactivate

ENG-11544

ENG-11943_Floats.feature

Float requests, status assertions, active/historical float counts

ENG-11943

ENG-12341_Float_Collections.feature

Float collections batch processing: daily collection runs, outcome and history verification

ENG-12341