How to Seed a Staging Database With Realistic Test Addresses

Copying the production database into staging is the fastest way to get realistic test data and the fastest way to turn a staging bug into a privacy incident. This is the version that does not do that.

Last updated: 30 August 2026 · Tools SoShareIT editorial team

What “realistic” actually needs to mean

Test data does not need to be plausible to a human. It needs to be plausible to your code. Those are different bars, and aiming at the wrong one is why so many seed scripts are both more work and less useful than they should be.

Your validation layer cares about shape: does the postcode match the country, is the field within its length limit, does the character set survive the round trip. It does not care whether the street name sounds like a real street. A seed script that invests in convincing street names and gets the postcode format wrong has optimised the part nobody checks.

Keep the fields consistent with each other

The most common seeding bug is fields that are individually valid and collectively impossible. A row with country US, state NY and postcode 90210 passes three separate field validators and fails the moment anything checks them together.

Meshing brass gears illustrating address fields that must stay consistent with each other

That row will make an address-verification integration look broken when it is working correctly, and the hour spent debugging it is spent on the fixture rather than the code.

Two rules prevent nearly all of it:

  • Pull related fields from the same source row. If you take the state, take the postcode range that belongs to it. The state-by-state ZIP table on Tools SoShareIT exists for exactly this — each row pairs a state with the range that genuinely belongs to it.
  • Generate the postcode after the country, never before. Country determines format. Choosing the code first and the country second produces Canadian postcodes in Germany, and it is a surprisingly easy loop to write by accident.

Seed the edge cases on purpose

A thousand rows of the same shape test one thing a thousand times. The rows that find bugs are the awkward ones, and they only exist in your fixture if you put them there deliberately.

One jagged stone among smooth pebbles illustrating deliberately seeded edge case rows

Swipe the table sideways to see every column.

Seed a row with It catches
A postcode with a letter (K1A 0B1) Digits-only validation and numeric column types
A postcode with a hyphen (100-0001) Over-eager sanitising that strips the separator
A country with no postcode at all A required field that should be conditional
A leading zero (01001) Integer casting that eats the zero
Accents and non-Latin characters Encoding that survives insert but not display
A name at the column length limit Silent truncation

The leading-zero row is worth singling out. Massachusetts postcodes start 01, and a column typed as an integer turns 01001 into 1001 on the way in. Everything reports success. The data is simply wrong, and it stays wrong until a customer in Massachusetts complains.

Make it obvious the data is fake

At some point somebody will look at a staging record and not immediately know whether it is real. Design for that moment, because the alternative — emailing a test account that turns out to belong to a customer — is the failure this whole approach exists to prevent.

Use reserved values for anything that could reach the outside world. Email addresses on example.com, hostnames on .test, IP addresses in 192.0.2.0/24, phone numbers in the 555-0100 to 555-0199 block. Every one of those is guaranteed to belong to nobody, so a seed script that escapes into a real send queue does no damage.

Our guide to safe IP ranges and reserved domains covers which block to use for which job, including the difference between private and reserved space — a distinction that matters more than it looks.

Make the seed reproducible

Random data that is different on every run produces test failures nobody can reproduce. Seed your random number generator from a fixed constant, and record it. When a test fails on row 4,812, you want to be able to regenerate exactly that row rather than shrug at it.

Keep the awkward rows from the table above in a small fixed fixture, checked into the repo, separate from the bulk generated set. Those rows are the regression suite. The bulk data is for volume, and volume is the part that can safely change between runs.

For one-off rows while you are writing the script, the generator on Tools SoShareIT gives you a format-correct code for any of 15 countries in a click. Just keep the distinction in mind: it produces format-valid data, never delivery-valid data, which is exactly what a staging database should contain.

Frequently asked questions

Can I just copy the production database into staging?

It is the fastest option and the one most likely to end in an incident report. The data keeps every obligation it had in production and loses every control that protected it there. Our guide on why real customer records do not belong in staging sets out the four ways it goes wrong in practice.

How many rows do I actually need?

Fewer than you think for correctness, more than you think for performance. Validation and encoding bugs surface in a handful of deliberately awkward rows. Index and cache behaviour needs production-scale volume with production-like distribution — volume alone tests the wrong bottleneck.

Why did my leading zeros disappear?

An integer column. Massachusetts postcodes start 01, and 01001 inserted into an integer field becomes 1001 with no error reported anywhere. Store postal codes as text, always, and seed a leading-zero row so the test suite catches it if someone changes the type.

Should the generated data be the same on every run?

Yes, unless you have a specific reason otherwise. Seed your random number generator from a recorded constant. Data that differs every run produces failures nobody can reproduce, and “it passed on my machine” becomes literally true and completely useless.

How do I stop staging from emailing real people?

Use reserved values rather than a guard flag, because guard flags get toggled during debugging and not toggled back. Addresses on example.com are undeliverable by design, so a seed script that escapes into a live send queue reaches nobody at all.