Generating realistic Java test data with Instancio
Creating test data by hand becomes tedious as soon as a Java application contains more than a few related objects. Testers and developers end up maintaining large fixture files that are difficult to understand, become stale when models change, and can hide the exact conditions needed by an individual test. Synthetic test data offers a more flexible alternative, provided it remains predictable and relevant to the domain.
Instancio is a Java library designed specifically for this purpose. It builds complete object graphs, populates nested objects and collections, and allows individual fields to be controlled where a test needs precise values. This combination makes it useful for unit tests, REST API testing, asynchronous workflows, performance tests, and continuous delivery pipelines where fast, repeatable feedback matters.
Why synthetic data needs domain control
A conventional fixture gives complete control over every value, but that control carries a cost. When a domain model changes, every affected fixture may need updating. It is also easy for a shared fixture to contain unnecessary details, so a test focused on one validation rule still has to understand addresses, account balances, dates, and identifiers that are irrelevant to it.
Instancio takes a different approach by generating values from the class structure while allowing selected fields to be pinned. The resulting object can satisfy the basic shape expected by production code without requiring a separate file for every scenario. A test can specify only the customer status or loyalty tier it cares about and let the library populate the remaining fields.
Generated values still need domain judgement. A random postcode, phone number, or timestamp may be structurally valid while violating a business rule. The strongest test-data strategy therefore combines broad generation with explicit control for values that affect assertions, integrations, and realistic Australian scenarios. Instancio supplies the mechanics; the test author remains responsible for meaning.
Building a reusable Instancio model
An Instancio model describes how instances of a class should be created. It works with ordinary Java objects and can traverse fields, nested objects, arrays, collections, and supported record layouts. Field selectors identify exactly where an override belongs, avoiding fragile reflection calls or positional constructor arguments.
Consider a Customer object containing an identifier, email address, phone number, address, balance, and loyalty tier. A basic instance can be created with a few explicit values:
Model<Customer> customerModel = Model.of(Customer.class)
.set(field(Customer::getCustomerId), "CUST-004218")
.set(field(Customer::getBalance), new BigDecimal("248.75"));
Customer customer = Instancio.of(customerModel).create();
The model acts as a reusable data blueprint. A unit-test package can define one model for valid customers and another for specific states, such as a suspended account. Test methods can then override only the fields relevant to the behaviour under examination. Keeping the model in a shared test utility also gives teams one place to adjust data conventions.
Collections and nested values need deliberate treatment. Instancio can populate a Set<String> or a List<PaymentMethod>, but an empty collection may sometimes represent a clearer test condition than generated elements. The same applies to nested addresses: supplying a complete address is useful when a postcode affects behaviour, while allowing the nested graph to be generated is useful when only the customer type matters.
Making generation deterministic and realistic
Randomness increases coverage, but it should not make a failed test impossible to reproduce. Instancio supports seeded generation through its settings, allowing the same class model and seed to produce the same values during a build:
Customer customer = Instancio.of(Customer.class)
.withSettings(Settings.create()
.seed(20250308L)
.locale(new Locale("en", "AU")))
.set(field(Customer::getEmail), "maya.chen@example.com.au")
.create();
A fixed seed provides repeatability within the same Instancio version and runtime configuration. Teams should record the seed with a failure and avoid upgrading the library casually when reproducibility is important. Strict stubbing can also detect configured values that were never applied, although it is most useful in focused unit-test suites rather than broad exploratory tests.
Australian realism requires a few domain-specific details. Postcodes should remain four-character strings: 2000 can represent Sydney CBD, 3000 Melbourne CBD, and 4000 Brisbane CBD, while a value such as 0800 shows why an integer is unsuitable for some postcodes. Phone suppliers can use the +61 country code and an Australian mobile format such as +61 412 345 678. Money should be represented in Australian dollars using BigDecimal or integer cents, with two decimal places where cents are applicable. Date and time values should use an Australian time zone such as Australia/Sydney or Australia/Melbourne when daylight-saving boundaries matter, rather than relying implicitly on the server’s default zone.
Boundary cases deserve more attention than ordinary generated values. Tests may need an empty collection, a maximum-length identifier, a customer created near a leap day, or a timestamp around the Melbourne daylight-saving transition. Apply these as deliberate overrides and validate the result with domain constraints or assertions. A generated value is useful only when the test states the condition it is meant to exercise.
Reusable patterns for Australian test data
Instancio accepts suppliers for values that require more structure than a constant. This is useful for generating repeatable phone numbers, email addresses, postcodes, or reference identifiers without embedding formatting logic throughout the test suite:
Customer customer = Instancio.of(Customer.class)
.set(field(Customer::getPhone),
() -> String.format(Locale.ROOT, "+61 4%08d", 12345678))
.create();
A supplier can remain fixed for a particular test, while a custom provider can produce controlled variation. Any randomness inside that provider should also be seedable; otherwise the main Instancio seed will not make the whole result reproducible. For service objects, providers can populate nested account, address, and preference graphs according to Australian business conventions.
Several values benefit from being pinned explicitly:
- Identifiers and account references that must match database rows or API stubs
- Monetary amounts expressed in Australian dollars with a defined scale
- Contact details using
+61, local phone conventions, and suitable.com.audomains - Dates and times that exercise Australian business hours or daylight-saving boundaries
The opposite approach is appropriate for fields that merely need a plausible value. Names, marketing preferences, internal notes, and unrelated reference codes can usually be generated. This reduces noise in a test while retaining enough variation to expose assumptions such as an unexpected string length or an invalid enum combination.
Diagnostics become especially important when a large object graph contains several custom providers. Recording a small amount of context allows a failed run to be reproduced without preserving every piece of production data:
- The random seed, Instancio version, and relevant Java runtime version
- Every field overridden from the base model
- The test name, endpoint, generation time, and number of objects created
Comparing approaches and standardising the workflow
Several test-data techniques can coexist, but they solve different problems. Hand-written fixtures offer complete control, generic random data provides breadth, and seeded Instancio models combine structure with repeatability. Custom providers add domain accuracy at the cost of maintaining a small amount of test-specific code.
| Approach | Level of control | Best fit | Main limitation |
|---|---|---|---|
| Hand-written fixtures | Complete | A small set of stable scenarios | High upkeep and duplicated data |
| Unseeded random data | Low | Broad shape and exploratory checks | Failures are difficult to reproduce |
| Seeded Instancio models | High | Unit tests and repeatable API scenarios | Domain rules still need explicit control |
| Instancio with custom providers | Very high | Australian formats and integration contracts | Providers require maintenance and testing |
A practical workflow uses explicit values for business invariants, seeded generation for supporting attributes, and custom providers for local formats. The same base model can support unit tests, mocked REST calls, and integration fixtures, while suite-specific models introduce empty collections or exceptional states. In continuous delivery, this separation makes failures easier to diagnose because the seed, overrides, and generated object count are all available to the build log.
The implementation can begin inside one bounded package rather than becoming an organisation-wide framework immediately. A small factory can build the base model, apply suite-specific overrides, and attach generation details to assertion failures. The next concrete step is to add a seeded Instancio factory to one unit-test package and compare two customers generated during the same build.