Manual UAT testing#
Goal. Reproducible test cases for QA and stakeholders. “A subscription with three failed payments” should be one command, not a fifteen-minute click-through.
Why Orchard. UAT scripts usually live as a README page of SQL snippets, a shared Notion doc, or inside someone’s head. Turning them into scenarios makes them runnable, version-controlled, and reviewable.
Pattern: test case per scenario#
Each distinct UAT case becomes a scenario file. Name them after the test case:
scenarios/
├── uat/
│ ├── subscription_three_failures.hcl
│ ├── refund_across_currencies.hcl
│ ├── merchant_account_locked.hcl
│ └── high_risk_chargeback.hclExample: subscription with three failures#
scenario "uat_subscription_three_failures" {
required_providers {
postgres = { source = "builtin/postgres" }
}
variable "dsn" {}
variable "customer_email" {
default = "uat+subs3fail@example.com"
}
provider "postgres" {
dsn = var.dsn
}
component "customer" "subject" {
source = "../../components/customer.hcl"
inputs = {
email = var.customer_email
name = "UAT Customer"
}
}
component "subscription" "plan" {
source = "../../components/subscription.hcl"
inputs = {
customer_id = component.customer.subject.id
plan = "pro_monthly"
status = "past_due"
}
}
action "postgres_query" "failed_attempt_1" {
query = "INSERT INTO payment_attempts (subscription_id, status, failure_code, attempted_at) VALUES (${component.subscription.plan.id}, 'failed', 'card_declined', NOW() - INTERVAL '14 days')"
}
action "postgres_query" "failed_attempt_2" {
query = "INSERT INTO payment_attempts (subscription_id, status, failure_code, attempted_at) VALUES (${component.subscription.plan.id}, 'failed', 'insufficient_funds', NOW() - INTERVAL '7 days')"
}
action "postgres_query" "failed_attempt_3" {
query = "INSERT INTO payment_attempts (subscription_id, status, failure_code, attempted_at) VALUES (${component.subscription.plan.id}, 'failed', 'card_declined', NOW() - INTERVAL '1 day')"
}
output "customer_email" { value = component.customer.subject.email }
output "subscription_id" { value = component.subscription.plan.id }
}A QA engineer runs this and gets a login email plus the exact state described in the test plan.
Tips#
Unique identifiers per run. Use variables with sensible defaults, but override them for each run so tests don’t collide:
orchard run scenarios/uat/subscription_three_failures.hcl \ --var customer_email="uat+$(date +%s)@example.com"Surface what QA needs in outputs: customer ID, login URL, API token. Don’t make testers run a second SQL query to find the row they just created.
Use
lifecycle { teardown { } }instead of cleanup scripts. Attach a teardown block to eachactionand letorchard teardown <record>undo the run:action "postgres_query" "create_subscription" { query = "INSERT INTO subscriptions (...) RETURNING id" lifecycle { teardown { query = "DELETE FROM subscriptions WHERE id = ${self.id}" } } }The record file
orchard runwrites is allorchard teardownneeds — no bespoke cleanup scenario, no hand-rolled shell script. See lifecycle.Run UAT scenarios in CI. Even if the real tests are manual, having CI verify that each scenario still applies cleanly catches schema drift early.