How to write Test Data Builders in Apex?

I need to setup test data in proprietary cloud platform (Salesforce.com) where I have no access to advanced testing frameworks. Besides that you can read my code example like it would be Java.

Setting up data basically means to insert records of a quite complex object structure into the database. I decided to go with the Test Data Builder pattern described in this blog post and the related book.

It took me a while to realize that this makes everything much harder as you have to decide where to put your actual DML (meaning when do you insert the record you build).

If you do it naively in the build() I see no way to optimize / bulkify DML statements – which would be very important at the Salesforce.com platform.

On the other hand if you pass around Builder instances instead of Record instances around and build everything in the end you need extra methods to pass (in-memory but no yet persisted records) around.

@isTest
public class OpportunityBuilder {

  private Account parentAccount
    public Opportunity record;
    private List<LineItemBuilder> lineItemBuilders;


  public OpportunityBuilder(Account acc) {
    record = new Opportunity();
    record.Name = ...
    record.Account = acc.Id; 
    ...
    lineItemBuilders = new List<LineItemBuilder>();
  }


  public OpportunityBuilder add(LineItemBuilder lib) {
    lineItemBuilders.add( lib );
    return this;
  }


  public Opportunity build() {
    insert record;

    List<LineItem> lis = new List<LineItem>();
    for(LineItemBuilder builder : lineItemBuilders) {
        LineItem li = builder.record;
        li.Opportunity = record.Id;  // Id requires previous DML
        lis.add(li);
    }
    insert lis;

    return record;
  }
}

My question now is:

  1. How can I make my Builder classes super-extensible? Delay DML and pass Builders instead of records?
  2. Do you know of any resource which enhances the above pattern for database use?
  3. What other solutions do you know that would be an alternative?

Answer

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());

Attribution
Source : Link , Question Author : Robert Sösemann , Answer Author : Robert Sösemann

Leave a Comment