This guide introduces the core concepts of XFTY and demonstrates the most common ways of generating test data.
After reading this guide you should be comfortable:
- generating records
- customizing individual fields
- creating related records
- understanding Bundles
- choosing insert modes
- deciding when relationships should be created
More advanced topics such as implementing Providers and writing custom value expressions are covered in later guides.
InsertMode.Nowinserts for real, through whateverIPersistenceGatewayyou configure (see insert-modes); with none configured it throws rather than silently doing nothing. Everything below usesMockinstead — realistic-looking Ids, nothing persisted — since that's what a unit test usually wants.
The simplest way to use XFTY is to request an object from a Provider.
using Net.NowhereAtAll.Xfty.Core; // InsertMode, InsertInclusivity, …
using Net.NowhereAtAll.Xfty.Core.RecordProviders; // RecordProvider, RecordProvider<TRecord>, IRecordProvider
using Net.NowhereAtAll.Xfty.Demo;
DefaultProviderLookup lookup = new();
Contact contact = await new RecordProvider<Contact>(lookup)
.Supply();This creates a single Contact. The generic RecordProvider<Contact> returns
a typed record — no cast. The non-generic new RecordProvider(typeof(Contact), lookup)
is equivalent but its Supply() returns object. Both live in
Net.NowhereAtAll.Xfty.Core.RecordProviders.
By default:
- one object is generated
- no records are persisted
- no related records are generated
- default values are supplied automatically
The returned object is immediately ready for use in your test.
A Provider is responsible for generating test data for a particular record type.
For example:
- a
ContactProvider knows how to create Contacts - an
AccountProvider knows how to create Accounts - a
CaseProvider knows how to create Cases
Tests never need to know how these objects are constructed. They simply request the object type they need.
Internally, Providers use centrally-defined Master Templates to populate required fields and relationships.
A Provider only knows what type of object you want.
A Provider Lookup knows which Provider should be used to generate it.
DefaultProviderLookup lookup = new();
RecordProvider<Contact> provider = new(lookup);Separating Providers from Provider Lookups lets an application register different Provider implementations without modifying the framework itself.
DefaultProviderLookup (Net.NowhereAtAll.Xfty.Demo) is this port's own
starter-kit lookup — a working example to copy and adjust for your project, not
a base class to extend. See extend/provider-lookups.
Most tests only care about one or two fields.
Instead of constructing an entire record, provide an Override Template containing only the values relevant to your test.
Contact contact = await new RecordProvider<Contact>(lookup)
.SetOverrideTemplate(new Contact { FirstName = "Alice", LastName = "Smith" })
.Supply();XFTY preserves the supplied values while generating everything else automatically.
For example, if the Master Template specifies a default email address, that value will still be generated.
If the Override Template specifies an email address, the Override Template always wins.
Three constructor overloads save a call for the most common starting points:
// from a template - derives the record type (and any Provider variant) from it
new RecordProvider(new Contact { FirstName = "Alice" }, lookup);
// from a list of templates - derives the record type from the first
new RecordProvider([new Contact(), new Contact()], lookup);
// from a lookup key - derives the record type from the key and pins that variant
new RecordProvider(LookupKey.Get<Contact>(), lookup);They are exactly equivalent to the (Type, lookup) constructor followed
by SetOverrideTemplate(...) / SetOverrideTemplateList(...) / WithVariant(...).
Lookup keys and variants are covered in provider-variants.
There are two ways to create multiple records.
The simplest is to specify a quantity.
List<Contact> contacts = await new RecordProvider<Contact>(lookup)
.SetQuantityPerTemplate(5)
.SupplyList();This generates five Contacts using the same template.
If each generated record should differ, use an Override Template List instead.
List<Contact> contacts = await new RecordProvider<Contact>(lookup)
.SetOverrideTemplateList([
new Contact { FirstName = "Alice" },
new Contact { FirstName = "Bob" },
])
.SupplyList();When both a quantity and an Override Template List are supplied, every template is generated the requested number of times.
Relationship generation is controlled independently from persistence.
Bundle bundle = await new RecordProvider<Contact>(lookup)
.SetInsertMode(InsertMode.Mock)
.SetInclusivity(InsertInclusivity.Required)
.SupplyBundle();The resulting Bundle contains both the requested Contacts and any related records generated during the operation.
object contact = bundle.GetList<Contact>(x => x.Id)![0];
object account = bundle.GetList<Contact>(x => x.AccountId)![0];Bundle
├── Contact
└── Account
The generated Contact automatically references the generated Account.
Bundles are the primary data structure returned by XFTY.
Rather than returning only the requested records, Bundles contain the entire object graph created during generation.
For example, generating a Case may also generate:
Case
├── Account
└── Contact
Bundles make every generated object available without requiring additional lookups.
Lists are extracted using the relationship field that produced them.
List<object> accounts = bundle.GetList<Case>(x => x.AccountId)!;Nested Bundles can also be traversed.
Bundle? accountBundle = bundle.GetBundle<Case>(x => x.AccountId);Generating objects and persisting objects are separate concerns.
XFTY supports five insert modes, plus one orthogonal setting that composes with any of them.
| Mode | Description |
|---|---|
Never |
Generate records without Ids. |
Mock |
Generate realistic-looking Ids without any persistence. |
Now |
Insert every generated record through the configured IPersistenceGateway. Throws if none is configured. |
Later |
Behaves like Never while documenting that insertion will happen later. |
Deferred |
Generate like Never over many calls, registering everything for a single later flush; see deferred-insert. |
.ExcludePrimaryIds() leaves just this call's own primary un-Id'd, whatever
mode it's combined with, while every ancestor it needs is still persisted
normally - see insert-modes.
For most tests today:
| Test type | Recommended mode |
|---|---|
| Unit Test | Mock |
Because generated mock Ids do not point at real records, tests should never
treat a Mock record as if it were persisted.
Relationship generation is controlled independently from insertion.
| Mode | Description |
|---|---|
None |
Create no related records. |
Required |
Create only required relationships. |
All |
Create required and optional relationships. |
PreventCascade |
Create only the first level of relationships. |
Required is recommended for most tests.
It produces enough related data for records to be valid without generating unnecessary object graphs.
Every Provider ultimately generates a Bundle.
The convenience methods simply extract data from that Bundle.
| Method | Returns |
|---|---|
Supply() |
First generated record |
SupplyList() |
Primary generated records |
SupplyBundle() |
Entire generated object graph |
If your test only needs the requested records, Supply() or SupplyList() are
usually sufficient.
If your test needs to inspect related records, use SupplyBundle().
Now that you understand the basic workflow, each feature has its own page — see the feature matrix.
- override-templates · value-expressions · context-aware-values — customizing generated data
- relationships · per-call-relationships · shared-ancestors · bundles — object graphs
- insert-modes · deferred-insert — persistence
- advanced/ — combining features
To teach XFTY about a new record type, see extend/providers.
Runnable: RecordProviderIntegrationTest, RecordFactoryTest