Test data management sounds like something a platform team handles for you. At most small companies, no such team exists, so QA owns it by default — quietly, without a mandate, one broken staging account at a time. After enough mornings spent wondering why yesterday's test user vanished, I built a lightweight system. It has three parts: seed scripts, deliberate edge-case data, and state resets.
Realistic data beats production copies
The tempting shortcut is copying production into staging. I avoid it. Production dumps carry real names, emails, and phone numbers into an environment with weaker access controls, which is a privacy problem before it's a testing problem. They're also huge, stale within a week, and full of accidental data that makes failures hard to reason about.
What I want instead is a small, realistic data set I fully understand: twenty users whose states I chose on purpose — new account, verified account, account with an abandoned cart, account mid-subscription. When a test fails, I know exactly what the data looked like before the run, which is half of debugging.
Test data management starts with seeding scripts
A seed script is any repeatable way to build that known state. Mine is a set of API calls in a Postman collection plus a few SQL inserts for things the API can't create. It doesn't need to be elegant; it needs to be rerunnable. The rule I hold myself to: if I create test data by clicking through the UI, I'm borrowing time from future me. Clicking is fine once — the second time, script it.
Edge-case data sets: long names, Arabic, empty states
Happy-path data finds happy-path bugs. My seed data deliberately includes the inputs that break layouts and logic:
- Long values — a 70-character name and a long email, for truncation and overflow bugs.
- Arabic text — as a native speaker I always seed RTL strings, because bidirectional text exposes layout and encoding bugs that Latin-only data never touches.
- Empty states — a user with zero orders, zero notifications, an empty list everywhere a list exists. Empty states are the least-tested screens in most apps.
- Boundary values — zero-amount transactions, quantities at the documented maximum, dates far in the past.
This costs nothing extra: the edge cases live in the same seed script and get recreated on every run.
Resetting state between runs
Automation that mutates data will poison its own next run. My Maestro flows launch the app with cleared local state, but backend state needs its own answer. Options, cheapest first: use throwaway accounts created per run with a timestamp suffix; add teardown calls that delete what the test created; or reset the whole environment on a schedule if you're lucky enough to have one nobody else shares. I mostly use per-run accounts — they never collide, and stale ones can be swept later.
SQL checks close the loop
The UI can lie; the database doesn't. A few saved queries verify that seeding worked and that tests did what they claim: a count of seeded users by state, a check that no orphaned orders point at deleted accounts, a select on the newest records after a test run to confirm the writes actually happened. Simple SELECT COUNT(*) and GROUP BY queries catch data drift days before it turns into a mysterious red build.
Start smaller than feels respectable: one seed script, one edge-case user with an Arabic name, one SQL sanity query. A tiny system you actually rerun beats a database strategy document nobody reads.
None of this required permission, budget, or a dedicated team. It required admitting that test data is part of the test, and treating it with the same care as the assertions.