An SObject fabrication API to reduce database interactions and dependencies on triggers in Apex unit tests. It includes the ability to fabricate system and formula fields, rollup summaries, and child relationships. Strongly inspired by Stephen Willcock's Dreamforce presentation on Tests and Testability in Apex.
How many times do you have to execute a query to know that it works? - Robert C. Martin (Uncle Bob)
Databases and SOQL are slow, so we should mock them out for the majority of our tests. We should be able to drive our SObjects into the state in which the system can be tested.
In addition to queries, how many times do we need to test our triggers to know that they work?
Rather than relying on triggers to populate fields in our unit tests, or the results of a SOQL query to populate relationships, we should manually force our SObjects into the state in which the system can be tested.
SObjectFabricator provides the ability to set any field value, including system, formula, rollup summaries, and relationships.
Creating an SObject and setting properties on it can be as simple as:
Account accountSobject = (Account)new sfab_FabricatedSObject( Account.class )
.set( 'Name' , 'Account Name' )
.set( 'LastModifiedDate', Date.newInstance( 2017, 1, 1 ) )
.toSObject();
Creating an SObject with a Lookup or Master / Detail relationship set can be as simple as:
Contact contactSobject = (Contact)new sfab_FabricatedSObject( Contact.class )
.set( 'LastName' , 'PersonName' )
.set( 'Account.Name', 'Account Name' )
.toSObject();
Or you can explicitly set the object by using another fabricated SObject and setting the relationship:
sfab_FabricatedSObject fabricatedAccount = new sfab_FabricatedSObject( Account.class )
.set( 'Name', 'Account Name' );
Contact contactSobject = (Contact)new sfab_FabricatedSObject( Contact.class )
.set( 'Account', fabricatedAccount )
.toSObject();
Creating an SObject with a child relationship set can be as simple as:
Account accountSobject = (Account)new sfab_FabricatedSObject( Account.class )
.set( 'Name' , 'Account Name' )
.add( 'Contacts', new sfab_FabricatedSObject( Contact.class )
.set( 'LastName', 'PersonName' ) )
.add( 'Contacts', new sfab_FabricatedSObject( Contact.class )
.set( 'LastName', 'OtherPersonName' ) )
.toSObject();
There are lots of other options. With SObjectFabricator, you can:
sfab_FabricatedSObject fabricatedAccount = new sfab_FabricatedSObject( Account.class );
// Set fields using SObjectField references, including those not normally settable:
fabricatedAccount.set( Account.Id, 'Id-1' );
fabricatedAccount.set( Account.LastModifiedDate, Date.newInstance( 2017, 1, 1 ) );
// Set fields using the names of the fields:
fabricatedAccount.set( 'Name', 'The Account Name' );
// Set lookup / master detail relationships explicitly
fabricatedAccount.set( 'Owner', new sfab_FabricatedSObject( User.class ).set( 'Username', 'TheOwner' ) );
// Set lookup / master detail relationships implicitly
fabricatedAccount.set( 'Owner.Alias', 'alias' );
// Set multi-leveled lookup / master detail relationships implicitly
fabricatedAccount.set( 'Owner.Profile.Name', 'System Administrator' );
// Set child relationships in one go
fabricatedAccount.set( 'Opportunities', new List<sfab_FabricatedSObject> {
new sfab_FabricatedSObject( Opportunity.class ).set( 'Id', 'OppId-1' ),
new sfab_FabricatedSObject( Opportunity.class ).set( 'Id', 'OppId-2' )
});
// Set child relationships one-by-one
fabricatedAccount.add( 'Opportunities', new sfab_FabricatedSObject( Opportunity.class ).set( 'Id', 'OppId-3' ) );
fabricatedAccount.add( 'Opportunities', new sfab_FabricatedSObject( Opportunity.class ).set( 'Id', 'OppId-4' ) );
// Generate an SObject from that configuration
Account sObjectAccount = (Account)fabricatedAccount.toSObject();
// Account:{LastModifiedDate=2017-01-01 00:00:00, Id=Id-1, Name=The Account Name}
System.debug( sObjectAccount );
// User:{Username=TheOwner, Alias=alias}
System.debug( sObjectAccount.Owner );
// Profile:{Name=System Administrator}
System.debug( sObjectAccount.Owner.Profile );
// (Opportunity:{Id=OppId-1}, Opportunity:{Id=OppId-2}, Opportunity:{Id=OppId-3}, Opportunity:{Id=OppId-4})
System.debug( sObjectAccount.Opportunities );
Each of the mechanisms also allow you to navigate through parent structures:
sfab_FabricatedSObject fabricatedContact = new sfab_FabricatedSObject( Contact.class );
fabricatedContact.set( 'Account.Name', 'The Account Name' );
fabricatedContact.set( 'Account.Owner', new sfab_FabricatedSObject( User.class ) );
fabricatedContact.set( 'Account.Opportunities', new List<sfab_FabricatedSObject> {
new sfab_FabricatedSObject( Opportunity.class ).set( 'Id', 'OppId-1' ),
new sfab_FabricatedSObject( Opportunity.class ).set( 'Id', 'OppId-2' )
});
fabricatedContact.add( 'Account.Opportunities', new sfab_FabricatedSObject( Opportunity.class ).set( 'Id', 'OppId-1' ) );
Contact sObjectContact = (Contact)fabricatedContact.toSObject();
The set methods form a fluent API, meaning you can condense the configuration along the lines of
Account sObjectAccount = (Account)new sfab_FabricatedSObject( Account.class )
.set( Account.Id, 'Id-1' )
.set( Account.LastModifiedDate, Date.newInstance( 2017, 1, 1 ) )
.set( 'Name', 'The Account Name' )
.set( 'Owner', new sfab_FabricatedSObject( User.class ).set( 'Username', 'TheOwner' ) )
.set( 'Opportunities', new List<sfab_FabricatedSObject> {
new sfab_FabricatedSObject( Opportunity.class ).set( 'Id', 'OppId-1' ),
new sfab_FabricatedSObject( Opportunity.class ).set( 'Id', 'OppId-2' )
})
.add( 'Opportunities', new sfab_FabricatedSObject( Opportunity.class ).set( 'Id', 'OppId-3' ) ) // though, it would be odd to combine set
.add( 'Opportunities', new sfab_FabricatedSObject( Opportunity.class ).set( 'Id', 'OppId-4' ) ) // and add on a single child relationship
.toSObject();
Field values can be set in bulk, either at construction time, or later (using set
), by creating a Map<SObjectField, Object>
of the fields that you wish to set and using that.
Map<SObjectField, Object> accountValues = new Map<SObjectField, Object> {
Account.Id => 'Id-1',
Account.LastModifiedDate => Date.newInstance(2017, 1, 1)
};
Account fabricatedViaConstructor = (Account)new sfab_FabricatedSObject(Account.class, accountValues)
.toSObject();
Account fabricatedViaSet = (Account)new sfab_FabricatedSObject(Account.class)
.set(accountValues)
.toSObject();
Field, Parent and Child Relationship values can be set in bulk, either at construction time, or later, by passing a Map<String,Object>
.
Map<String,Object> contactValues = new Map<String,Object> {
'Id' => 'Id-1',
'LastModifiedDate' => Date.newInstance(2017, 1, 1),
'Account.Name' => 'The Account Name',
'Owner' => new sfab_FabricatedSObject( User.class )
.set( 'Username', 'The Contact Owner' ),
'Account.Owner' => new sfab_FabricatedSObject( User.class )
.set( 'Username', 'The Account Owner' ),
'Opportunities' => new List<sfab_FabricatedSObject>{
new sfab_FabricatedSObject( Opportunity.class )
.set( 'Name', 'Contact Opportunity Name' )
},
'Account.Opportunities' => new List<sfab_FabricatedSObject>{
new sfab_FabricatedSObject( Opportunity.class )
.set( 'Name', 'Account Opportunity Name' )
}
};
Contact fabricatedViaConstructor = (Contact)new sfab_FabricatedSObject( Contact.class, contactValues )
.toSObject();
Contact fabricatedViaSet = (Contact)new sfab_FabricatedSObject(Contact.class)
.set( contactValues )
.toSObject();
If you prefer the set methods to be a little more explicit in their intention, you can use more specific versions of the set
method.
Account sObjectAccount = (Account)new sfab_FabricatedSObject( Account.class )
.setField( Account.Id, 'Id-1' )
.setField( 'LastModifiedDate', Date.newInstance( 2017, 1, 1 ) )
.setParent( 'Owner', new sfab_FabricatedSObject( User.class ).setField( 'Username', 'TheAccountOwner' ) )
.setParent( 'Contact.Owner', new sfab_FabricatedSObject( User.class ).setField( 'Username', 'TheContactOwner' ) )
.setChildren( 'Opportunities', new List<sfab_FabricatedSObject> {
new sfab_FabricatedSObject( Opportunity.class ).setField( Opportunity.Id, 'OppId-1' ),
new sfab_FabricatedSObject( Opportunity.class ).setField( Opportunity.Id, 'OppId-2' ) } )
.addChild( 'Opportunities', new sfab_FabricatedSObject( Opportunity.class ).setField( 'Id', 'OppId-3') ) // though, it would be odd to combine setChildren
.addChild( 'Opportunities', new sfab_FabricatedSObject( Opportunity.class ).setField( 'Id', 'OppId-4') ) // and addChild on a single child relationship
.toSObject();
Fields such as ContentVersion.VersionData
and Attachment.Body
are BASE64 encoded, and as such can normally be set by specifying with a Blob value.
When setting these fields using sfab_FabricatedSObject
, you may specify the value either:
Blob.valueOf( 'abc' )
'abc'
sfab_FabricatedSObject
will deal with the conversion to the correct data type for you.
When using SObjectFields in order to set field values, it is not possible for SObjectFabricator to ensure that fields from the correct object are being used. This is due to a limitation in the SObjectField class as supplied by Salesforce. I.E. SObjectField does not (at time of writing) include a reference to the object that the field belongs to, nor the method of construction.
Therefore, the following may not do quite as expected:
Contact con = (Contact) new sfab_FabricatedSobject( Contact.class )
.set( Contact.Account.Id, 'The Account Id?' )
.toSObject();
System.debug( 'Contact.Id: ' + con.Id );
// Contact.Id: The Account Id?
System.debug( 'Contact.Account.Id: ' + con.Account.Id );
// Contact.Account.Id: null
Similarly, the following yields the same misleading result:
Contact con = (Contact) new sfab_FabricatedSobject( Contact.class )
.set( Account.Id, 'The Account Id?' )
.toSObject();
System.debug( 'Contact.Id: ' + con.Id );
// Contact.Id: The Account Id?
System.debug( 'Contact.Account.Id: ' + con.Account.Id );
// Contact.Account.Id: null
In this case, the correct mechanism to use is a String representation of the field name. I.E.
Contact con = (Contact) new sfab_FabricatedSobject( Contact.class )
.set( 'Account.Id', 'The Account Id!' )
.toSObject();
System.debug( 'Contact.Id: ' + con.Id );
// Contact.Id: null
System.debug( 'Contact.Account.Id: ' + con.Account.Id );
// Contact.Account.Id: The Account Id!
It is not possible to set the field RawMessage
on the object EmailCapture
this is because of two issues combined:
EmailCapture.RawMessage
to be set directly - it will throw System.SObjectException: Field RawMessage is not editable
.Because of this combination, there is no way to set EmailCapture.RawMessage
for an in-memory object.