UPDATE: A much improved and real-life tested version can be found in this GitHub repo: https://github.com/rsoesemann/apex-domainbuilder
Thanks to Andrew Fawcett and his amazingly powerful UnitOfWork implementation I was able to create a base class to create amazingly simple Builder classes per Domain object.
Those not only have the general benefits of TestDataBuilders over Helper classes but also:
- Care about the DML as pure In-Memory tests are harder on Force.com
- Take care of bulkification and Governor Limits
- Could become an new Force.com Enterprise pattern always using the singular SObject names for custom objects
I'm planning on writing a blog post giving more code examples but I try to show the basics here.
Base class
public virtual class DomainObject {
private static fflib_SObjectUnitOfWork uow = initUnitOfWork();
public SObject record;
public DomainObject(SObject standalone) {
register(standalone);
}
public DomainObject(SObject child, Schema.SObjectField parentRelationship, DomainObject parent) {
this(child);
register(parentRelationship, parent.record);
}
public DomainObject(SObject linker,
Schema.SObjectField leftRelationship, DomainObject left,
Schema.SObjectField rightRelationship, DomainObject right) {
this(linker, leftRelationship, left);
register(rightRelationship, right.record);
}
public DomainObject persist() {
uow.commitWork();
uow = initUnitOfWork();
return this;
}
protected DomainObject addChild(DomainObject child, Schema.SObjectField parentRelationship){
uow.registerRelationship(child.record, parentRelationship, record);
return this;
}
private void register(SObject record) {
this.record = record;
if(record.Id == null) {
uow.registerNew(record);
}
}
private void register(Schema.SObjectField relationship, SObject relatedRecord) {
if(relatedRecord.Id == null) {
uow.registerRelationship(record, relationship, relatedRecord);
}
else {
record.put(relationship, relatedRecord.Id);
}
}
private static fflib_SObjectUnitOfWork initUnitOfWork() {
return new fflib_SObjectUnitOfWork(new List<Schema.SObjectType>{
Grandfather__c.SObjectType,
Father__c.SObjectType,
Child__c.SObjectType
});
}
}
Domain object with father and child records:
public class Father extends DomainObject {
private Father__c fht;
// CONSTRUCTORS
public Father(Parent par) {
super(new Father__c(), Father__c.mdr_Parent__c, par);
fht = (Father__c) record;
fht.txt_Field1__c = 'default';
//...
}
public Father() {
this(new Grandfather());
}
// BUILDER METHODS
public Father add(Child chl) {
return (Father) addChild(chl, Child__c.lkp_Father__c);
}
public Father addChild() {
return (Father) add(new Child());
}
//...many other builder methods
}
Usage in an integration test:
@isTest
private class MyApp_Test {
@isTest
private static void worksAsDefined() {
// Setup
Father bob = new Father()
.age(45)
.job('Clerk')
.add(new Child()
.age(22)
.gender(MALE)
.hobby(BASEBALL)
)
.persist();
// Exercise
doThisAndThat(bob.record);
// Verify
System.assert(allIsGood());
...
As long as the transaction completes successfully, any DML operations that completed successfully will remain so. Usually, this means that a developer did something like this:
try {
...
insert firstList;
...
insert secondList;
...
} catch(Exception e) {
System.debug(e.getMessage());
}
Since they caught the exception, the transaction completed successfully; if firstList completed okay but secondList threw an exception, then this pattern would result in a partial transaction like you describe.
The general rule is that you should not do this; always roll back the transaction if you want to use try-catch atomically, or don't catch the exception.
The general pattern for rollback is like this:
Database.SavePoint sp = Database.setSavePoint();
try {
...
insert firstList;
...
insert secondList;
...
} catch(Exception e) {
Database.rollback(sp);
ApexPages.addMessages(e);
}
This will prevent partial changes from being saved. Alternatively, don't use try-catch at all, but be aware that if you do this in a Visualforce page, it can result in a loss of "view state" when the page crashes, meaning the users won't have an opportunity to correct the error(s).
Best Answer
These methods give you additional options if you want to something other than the default behaviour the DML Statements give, such as...
allowFieldTruncation
assignmentRuleHeader
emailHeader
localeOptions
optAllOrNone
So you have three options
Depending on the options you want to control. You also get additional information returned from these methods that allow you to inspect the results of the operation at an individual record level, for example SaveResult. Here is the sample from the documentation around the use of the allOrNone option...