Skip to content

Latest commit

 

History

History
384 lines (238 loc) · 10.1 KB

File metadata and controls

384 lines (238 loc) · 10.1 KB

XFTY Internals

This document describes the internal architecture of XFTY and the design decisions behind it.

Most users only need the public API documented elsewhere. This guide is intended for developers who want to understand how the framework works internally, extend it, or contribute to its development.

Unlike the other documentation, this guide focuses on why the framework was designed the way it was rather than simply describing individual classes.


Design Goals

XFTY is first and foremost a test data factory.

Its goal is to make Salesforce test setup:

  • concise
  • maintainable
  • declarative
  • reusable

To achieve this, XFTY includes a small engine responsible for constructing complete object graphs, applying default values, generating related records, and optionally persisting them.

The engine exists so that test code doesn't have to.

A typical test should describe only the data it actually cares about, while XFTY supplies everything else.


High-Level Architecture

The overall architecture can be viewed as a pipeline.

Tests
   │
   ▼
XFTY_DummySObjectProvider
   │
   ▼
Provider Lookup
   │
   ▼
SObject Provider
   │
   ▼
Master Template
   │
   ▼
Factory Engine
   │
   ▼
Bundle

Each component has a single responsibility.

Component Responsibility
XFTY_DummySObjectProvider Public fluent API used by tests.
XFTY_DummySObjectProviderLookupIntf Resolves which Provider should generate a particular SObject.
XFTY_DummySobjectProviderIntf Describes how one SObject type should be generated.
XFTY_DummySObjectMasterTemplate Declarative description of default values and relationships.
XFTY_DummySObjectFactory Engine that constructs the object graph.
XFTY_DummySObjectBundle Represents the generated graph.
XFTY_DummyDefaultValueIntf Strategy interface for generating field values.
XFTY_DummyDefaultRelationship... Strategy for generating related records.
XFTY_IdMocker Generates realistic Salesforce Ids without DML.

Keeping these responsibilities separate makes each component relatively small and easy to reason about.


Declarative Rather Than Imperative

One of the fundamental design goals was to avoid imperative construction of test data.

Instead of writing code such as:

Account account = new Account(...);
insert account;

Contact contact = new Contact(...);
contact.AccountId = account.Id;
insert contact;

Providers instead declare what should exist.

new XFTY_DummySObjectMasterTemplate(Account.Id)
    .put(Account.Name,
         new XFTY_DummyDefaultValueIncrementingString("Account"))
    .put(Account.OwnerId,
         new XFTY_DummyDefaultRelationshipRequired(
             new User()
         ));

The framework is responsible for determining how that object graph should be created.

Separating the description of the graph from its construction makes Providers much smaller and easier to maintain.


Master Templates

The XFTY_DummySObjectMasterTemplate class is the declarative heart of XFTY.

A Master Template describes:

  • default field values
  • required relationships
  • optional relationships

Internally these are simply stored in three maps.

Default Values
Required Relationships
Optional Relationships

Each map is keyed by SObjectField, allowing the template to describe exactly how every field should be populated.

The fluent put(...) methods make Provider implementations concise while keeping the template itself immutable once cloned for use.


Why Relationships Are Keyed by SObjectField

Relationships are intentionally keyed by the field that stores the lookup Id, not by SObjectType.

This serves several purposes.

First, it tells XFTY exactly which field needs to be populated.

Second, it keeps graph construction and graph navigation consistent.

Finally, it naturally supports multiple relationships to the same object type.

For example:

PrimaryContact__c
SecondaryContact__c
BillingContact__c

may all reference Contact, but they represent different relationships.

Treating the field as the identity of the relationship avoids ambiguity throughout the framework.


Object Graphs

The Factory constructs complete object graphs rather than isolated records.

For example:

Contact
    │
    ▼
Account
    │
    ▼
Owner

Each relationship is represented by its own nested XFTY_DummySObjectBundle.

This preserves the recursive structure of the generated graph.

Destroying that hierarchy and flattening everything into collections would require additional work while losing useful structural information.


Why Bundles Contain Bundles

A Bundle stores two kinds of information.

  • the generated SObject instances
  • child Bundles representing generated relationships

This allows callers to retrieve either:

bundle.getList(Contact.AccountId)

or

bundle.getBundle(Contact.AccountId)

depending on whether they need the related records themselves or the entire subgraph beneath them.

Because the internal representation mirrors the generated graph, recursive construction becomes straightforward and the implementation remains simple.


Two-Phase Graph Construction

Object creation occurs in multiple phases.

Phase 1 – Create Objects

The Factory recursively creates every required SObject.

At this stage the objects exist, but relationships have not yet been wired together.

Phase 2 – Assign Ids

Depending on the selected insert mode, objects are either:

  • inserted
  • assigned mock Ids
  • left without Ids

Performing this as a separate phase allows every object at the current level to be inserted using a single DML operation rather than one insert per relationship or object type.

Phase 3 – Wire Relationships

Once related records possess Ids, lookup fields can be populated.

This separation greatly simplifies recursion while ensuring every lookup points at a valid record.


Value Providers

Rather than storing literal values, Master Templates store strategies for generating values.

Every value provider implements:

XFTY_DummyDefaultValueIntf

This allows generated values to be:

  • constant
  • incrementing
  • unique
  • calculated
  • completely custom

without changing any framework code.


Stateful Value Providers

Some value providers intentionally maintain internal state.

For example:

Account 1
Account 2
Account 3

is usually preferable to repeatedly generating:

Account 1
Account 1
Account 1

Similarly, unique email providers coordinate generation to avoid duplicate values.

This behaviour exists solely to improve the realism of generated data while keeping Provider implementations concise.


Immutability

XFTY clones templates aggressively.

Whenever records are generated, the framework creates new instances rather than modifying shared objects.

This avoids accidental sharing between generated records and prevents Providers from unexpectedly affecting one another.

Although cloning introduces a small amount of overhead, the improved predictability is well worth the cost in test code.


Mock Id Generation

One of XFTY's distinguishing features is its ability to generate realistic Salesforce Ids without performing DML.

XFTY_IdMocker combines:

  • the object's Salesforce key prefix
  • a fixed identifier
  • an incrementing counter

to produce unique 15-character Ids.

These Ids behave like normal Salesforce Ids for almost all testing purposes while avoiding the cost of database inserts.


Provider Lookup

Rather than using a global registry, XFTY requires callers to explicitly provide a Provider Lookup.

This allows different applications or packages to define different Provider collections.

One important use case is SFDX packaging.

Some Providers reference metadata that exists only within particular packages.

Separating Provider Lookups allows each package to expose only the Providers that it can successfully compile, avoiding cross-package compilation dependencies.

It also naturally supports different Provider sets for different projects, test suites, or organizational conventions.


Why getPrimaryTargetField() Exists

Most standard Salesforce objects identify records using the Id field.

However, not every Salesforce data type follows this pattern.

Rather than assuming every generated object can be identified by Id, Providers explicitly expose their primary target field.

This field is used internally when:

  • retrieving generated records from Bundles
  • wiring relationships
  • identifying the Provider's primary output

Although this is usually Id, using a configurable field makes the framework more flexible and avoids baking unnecessary assumptions into the engine.


Record Types

Record Types are one area where XFTY intentionally remains simple.

Provider Lookup currently maps a single Provider to each SObjectType.

As a result, supporting multiple Record Types for the same object currently requires custom Provider logic.

A common approach is for the Provider to inspect the override template and choose the appropriate Master Template internally.

Although somewhat manual, this keeps the framework itself relatively simple.


Design Trade-offs

Several implementation decisions intentionally favour simplicity over maximum flexibility.

Examples include:

  • every child currently receives its own generated parent
  • relationship generation is controlled by broad inclusion modes
  • Provider Lookup uses only SObjectType as its key

These choices keep the framework predictable while covering the overwhelming majority of testing scenarios.


Final Thoughts

XFTY intentionally separates describing test data from constructing test data.

Tests remain focused on the behaviour being verified.

Providers describe valid business objects.

The Factory constructs complete graphs.

Bundles preserve those graphs.

This separation of responsibilities keeps the public API compact while allowing the framework's internal engine to handle the complexity of generating realistic Salesforce test data.