Sunday 15 September 2013

Clone Plus Test Method Example

Well, it's been a while! I have been very busy, getting qualified as a certified advanced developer (see shiny new badge on the right hand bar :-o), sorting out my upcoming Dreamforce visit, reviewing Force.com books and promoting the South West Force.com group (more posts to come on these topics soon!).

Anyway, I figured a visit to clone plus was well overdue. For those unfamiliar with clone plus and what it does, see these previous posts introducing clone plus and some refinements that have been made along the way. The posts have proved quite popular, with lots of people providing feedback and asking questions. One of the most common questions that has appeared on the topic is test coverage. As is well known, all apex code needs at least 75% coverage to be promoted to a production org, and clone plus is no exception.

Below is a sample test class for clone plus, alongside the latest implementation of the page and controller (I have made some tweaks, mainly error handling). This is just an example of how you might go about testing a class like the clone plus controller. It is by no means an absolute complete testing solution, as it does not cover every scenario the user could find themselves in while using the controller.

Latest Version of Clone Plus Controller:
public class ClonePlusController {

  public List<relatedObjects> objectChildren  { get; set; }
  public String               objectTypeName  { get; set; }
  public String               objectName      { get; set; }
   
  @TestVisible private SObject headSObject, headClone;
  
  // Initialisation method called when the clone plus page is first loaded.
  // Use the id page parameter to find out what kind of object we are trying to clone.
  // Then load the object from the database.
  // Finally call the populateObjectChildren method to      
  public void initialiseObjectsForCloning()
  {

    // Here we generate a keyprefixmap using the global describe 
    // Then compare that to our object to determine type.  
    Map<String, Schema.SObjectType> gd = Schema.getGlobalDescribe(); 
       
    Map<String,String> keyPrefixMap = new Map<String,String>{};
          
    for(String sObj : gd.keySet()){
      Schema.DescribeSObjectResult r =  gd.get(sObj).getDescribe();
      keyPrefixMap.put(r.getKeyPrefix(), r.getName());
    }
      
    String objectID                 = ApexPages.currentPage().getParameters().get('id');
    String objectTypeKey            = objectId.subString(0,3);
      
    objectTypeName                  = keyPrefixMap.get(objectTypeKey);
      
    String primaryObjectQueryString = 'SELECT Id, Name FROM '
                                    + objectTypeName
                                    + ' WHERE Id = \''
                                    + objectId
                                    + '\'';
    
    headSObject = Database.query(primaryObjectQueryString);
    objectName          = '' + headSObject.get('Name');
    populateObjectChildren();    
  }

  // Get all of the children of the current object that have a object type
  // contained in the child object types page parameter.
  // Not restricting the child objects to particular types results in many
  // unclonable system objects being added to the possibiilites, which we need to avoid.
  // Making these object type choices also allows us 
  // pick and chose the specific kinds of objects we want to allow users to clone.  
  public void populateObjectChildren()
  {
       
    objectChildren = new List<relatedObjects>{};
        
    Set<String> childObjectTypes = new Set<String>{};
    
    // read the object types from the page parameter.    
    childObjectTypes.addAll(
         ApexPages.currentPage().getParameters().get('childobjecttypes').split(',')
    );
    
    // Use the sobjecttype describe method to retrieve all 
    // child relationships for the object to be cloned.    
    Schema.DescribeSObjectResult headDescribe = 
           headsObject.getSObjectType().getDescribe();
    
    List<Schema.ChildRelationship> childRelationships = 
           headDescribe.getChildRelationships(); 
    
    // Iterate through each relationship, and retrieve the related objects.       
    for (Schema.ChildRelationship childRelationship : childRelationships)
    {
      Schema.SObjectType childObjectType = childRelationship.getChildSObject();
      
      // Only retrieve the objects if their type is one of those we are interested in.          
      if (childObjectTypes.contains(childObjectType.getDescribe().getName()))
      {
        List<relatedObjectRow> relatedObjects = new List<relatedObjectRow>{};
                
        Schema.SObjectField childObjectField = childRelationship.getField();
                
        String relatedChildSObjectsquery = 'SELECT ID FROM ' 
                                         + childObjectType.getDescribe().getName()
                                         + ' WHERE '
                                         + childObjectField.getDescribe().getName()
                                         + ' = \'' 
                                         + headsObject.Id
                                         + '\''; 
                                                        
        for (SObject childObject : Database.query(relatedChildSObjectsquery))
        {
          relatedObjects.add(new relatedObjectRow(childObject));
        }
            
        if (!relatedObjects.isEmpty())
        {
          objectChildren.add(new relatedObjects(relatedObjects, 
                             childObjectType.getDescribe().getLabelPlural(), 
                             childObjectField.getDescribe().getName()));
        }  
      }
    }
  }
  
  // Perform the cloning process.
  // First clone the parent, then all of the child objects. 
  // Then redirect the user to the new object page.
  public PageReference doClone()
  {
   try
   {
      headClone = cloneObjects(new List<sObject>{headSObject})[0];
      insert headClone;
    
      cloneSelectedObjects();
    
      return new PageReference('/' + headClone.Id);
    }
    // If there is any kind of exception, then put a page message on the screen to inform the user.
    catch (exception e)
    {
      ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR, e.getMessage()));
      return null;
    }
  }
  
  // Clone the selected child objects.
  // Associate the cloned objects with the new cloned parent object.
  public void cloneSelectedObjects()
  {
        
    List<sObject> clonedObjects = new List<sObject>{};
    List<sObject> selectedRelatedObjects;
     
    for (relatedObjects relatedObject : objectChildren)
    {
      selectedRelatedObjects = new List<sObject>{};  
      clonedObjects = new List<sObject>{};  
      
      for (relatedObjectRow row : relatedObject.objectRows) 
      {
        if (row.selected)
        {
          selectedRelatedObjects.add(row.obj);
        }
      }
      
      if (!selectedRelatedObjects.isEmpty())
      {
        clonedObjects = cloneObjects(selectedRelatedObjects);
        
        for (sObject clone : clonedObjects)
        {
          clone.put(relatedObject.relatedFieldName, headClone.Id);  
        }
        
        insert clonedObjects;
      }
    }
  }

  // Clone a list of objects to a particular object type
  // Parameters 
  // - List<sObject> sObjects - the list of objects to be cloned 
  // The sObjects you pass in must include the ID field, 
  // and the object must exist already in the database, 
  // otherwise the method will not work.
  public static List<sObject> cloneObjects(List<sObject> sObjects){
                                                
    Schema.SObjectType objectType = sObjects.get(0).getSObjectType();
    
    // A list of IDs representing the objects to clone
    List<Id> sObjectIds = new List<Id>{};
    // A list of fields for the sObject being cloned
    List<String> sObjectFields = new List<String>{};
    // A list of new cloned sObjects
    List<sObject> clonedSObjects = new List<sObject>{};
    
    // Get all the fields from the selected object type using 
    // the get describe method on the object type.    
    if(objectType != null)
    {
      for (Schema.SObjectField objField : 
           objectType.getDescribe().fields.getMap().values())
      { 
        Schema.DescribeFieldResult fieldDesc = objField.getDescribe();
        // If the field type is location, then do not include it,
        // otherwise it will cause a soql exception.
        // Note that excluding the field does not stop the location from
        // being copied to the new cloned object.
        if(fieldDesc.getType() != DisplayType.LOCATION)
        {
          sObjectFields.add(fieldDesc.Name);
        }
      }
    }
    
    // If there are no objects sent into the method, 
    // then return an empty list
    if (sObjects != null || 
        sObjects.isEmpty() || 
        sObjectFields.isEmpty()){
    
      // Strip down the objects to just a list of Ids.
      for (sObject objectInstance: sObjects){
        sObjectIds.add(objectInstance.Id);
      }

      /* Using the list of sObject IDs and the object type, 
         we can construct a string based SOQL query 
         to retrieve the field values of all the objects.*/
    
      String allSObjectFieldsQuery = 'SELECT ' + String.join(sObjectFields,','); 
    
      allSObjectFieldsQuery += ' FROM ' + 
                               objectType.getDescribe().getName() + 
                               ' WHERE ID IN (\'' + sObjectIds.get(0) + 
                               '\'';

      for (Integer i=1 ; i < sObjectIds.size() ; i++){
        allSObjectFieldsQuery += ', \'' + sObjectIds.get(i) + '\'';
      }
    
      allSObjectFieldsQuery += ')';
    
      // Execute the query. For every result returned, 
      // use the clone method on the generic sObject 
      // and add to the collection of cloned objects
      for (SObject sObjectFromDatabase:
           Database.query(allSObjectFieldsQuery))
      {
        clonedSObjects.add(sObjectFromDatabase.clone(false,true));  
      }

    }
   
    return clonedSObjects;
    
  }
  
  // Related objects data construct - used to store a collection of child objects
  // connected to the head object through the same relationship field.
  public class relatedObjects
  {
    public List<relatedObjectRow> objectRows       { get; set; }
    public String                 pluralLabel      { get; set; }
    public String                 relatedFieldName { get; set; }
    
    public relatedObjects(List<relatedObjectRow> objectRows, 
                          String pluralLabel, 
                          String relatedFieldName) 
    {
      this.objectRows       = objectRows;
      this.pluralLabel      = pluralLabel;
      this.relatedFieldName = relatedFieldName;
    }   
  }     

  // An indidual child object row. It simple contains the object definition, 
  // and a checkbox to select the row for cloning on the clone plus page.
  public class relatedObjectRow
  {
    public sObject obj      { get; set; }
    public Boolean selected { get; set; }
    
    public relatedObjectRow(Sobject obj)
    {
      this.obj      = obj;
      // All object rows are selected by default.
      this.selected     = true;
    }
    
    public String getId(){
      try{
        return '' + obj.get('Id');
      } catch (Exception e){
        return '';
      }    
    }
  }
}


Latest Version of Clone Plus Page Code:
<apex:page controller="ClonePlusController" action="{!initialiseObjectsForCloning}">
    
  <apex:sectionHeader title="Clone Plus: {!objectName}"/>

  <apex:pageMessages />

  <apex:form id="theform" >
  
    Please select the child objects you would like to clone.<br/><br/>
  
    <apex:repeat value="{!objectChildren}" var="child">
      <apex:PageBlock title="{!child.pluralLabel}"> 
        <apex:pageBlockTable value="{!child.objectRows}" var="objectRow">
          <apex:column headerValue="Clone" width="10%">
            <apex:inputCheckbox value="{!objectRow.selected}"/>
          </apex:column>
       <apex:column headerValue="Id"   value="{!objectRow.Id}" width="45%"/>
        </apex:pageBlockTable>
      </apex:PageBlock>
    </apex:repeat>
    
    <apex:PageBlock >
      <apex:commandButton action="{!doClone}" value="Clone"/>
    </apex:PageBlock>
  </apex:form>  

</apex:page>


Sample Test Class:
/*
    A test method for the clone plus controller.
*/

@isTest
public class ClonePlusControllerTest
{
    /*
        Test the initialise objects for cloning method.
        
        Initialise an instance of the clone plus controller, and add some test parameters to the test URL.
        
        In this test, we will add an account Id as the the object to be cloned.
    */
    private static testmethod void testInitialiseObjectsForCloning()
    {
        // First lets create our sample Account object and some related Contacts.
        Account acc = new Account (Name = 'Test Account', Phone = '01482 123456789'); 
        insert acc;
        
        List<Contact> cons = new List<Contact>{};
        cons.add(new Contact(AccountId = acc.Id, FirstName = 'Sam', 
                             LastName = 'Ple' , email='test1@salesforce.com'));
        cons.add(new Contact(AccountId = acc.Id, FirstName = 'Ted', 
                             LastName = 'Test', email='test2@salesforce.com'));
        insert cons;
        
        List<Opportunity> opps = new List<Opportunity>{};
        opps.add(new Opportunity(AccountId = acc.Id, Name = 'Test Opportunity 1', 
                                 StageName='Closed Won' , CloseDate=Date.TODAY()));
        opps.add(new Opportunity(AccountId = acc.Id, Name = 'Test Opportunity 2', 
                                 StageName='Closed Lost', CloseDate=Date.TODAY()));
        insert opps;
        
        // Put the newly created account Id as the Id parameter on the page.
        ApexPages.currentPage().getParameters().put('Id', acc.id);
        
        // Also, add the contact object as a child object to clone.
        ApexPages.currentPage().getParameters().put('childobjecttypes','Contact,Opportunity');
        
        // Initialise a test instance of a clone plus controller.
        ClonePlusController cpc = new ClonePlusController();
        
        // Now call the initialise method.
        cpc.initialiseObjectsForCloning();
        
        // Now verify that all the class variables have been correctly set.
        system.assertEquals(2              , cpc.objectChildren.size());
        system.assertEquals('Contacts'     , cpc.objectChildren[0].pluralLabel);
        system.assertEquals('AccountId'    , cpc.objectChildren[0].relatedFieldName);
        system.assertEquals('Opportunities', cpc.objectChildren[1].pluralLabel);
        system.assertEquals('AccountId'    , cpc.objectChildren[1].relatedFieldName);
        
        // The related contacts should all be loaded into the controller.
        system.assertEquals(2              , cpc.objectChildren[0].objectRows.size());
        system.assertEquals(cons[0].Id     , cpc.objectChildren[0].objectRows[0].getId());
        system.assertEquals(cons[1].Id     , cpc.objectChildren[0].objectRows[1].getId());
        
        // The related opportunites should all be loaded into the controller.
        system.assertEquals(2              , cpc.objectChildren[1].objectRows.size());
        system.assertEquals(opps[0].Id     , cpc.objectChildren[1].objectRows[0].getId());
        system.assertEquals(opps[1].Id     , cpc.objectChildren[1].objectRows[1].getId());
        
        // All related objects should be selected for clonign by default.
        system.assert(cpc.objectChildren[0].objectRows[0].selected);
        system.assert(cpc.objectChildren[0].objectRows[1].selected);
        system.assert(cpc.objectChildren[1].objectRows[0].selected);
        system.assert(cpc.objectChildren[1].objectRows[1].selected);        

  // These variables all store information about the head object (Account)
        system.assertEquals('Account'   , cpc.objectTypeName);
        system.assertEquals(acc.Name    , cpc.objectName);
        system.assertEquals(acc.Id      , cpc.headSObject.Id);
        system.assertEquals(null        , cpc.headClone);
    }
    
    /*
     Test the "doClone" method. This method clones the parent object held in the controller,
     and all the related objects that have also been selected for copying.

        Initialise an instance of the clone plus controller, and populate the class variables with
        some sample objects. Select some of the related objects for cloning and ignore some others
  
        After the method has run, query the database to make sure that only those objects selected
        for cloning have been replicated.
    */
    private static testmethod void testDoCloneSuccess()
    {
        // First lets create our sample account object and some related Accounts.
        Account acc = new Account (Name = 'Test Account', Phone = '01482 123456789'); 
        insert acc;
        
        List<Contact> cons = new List<Contact>{};
        cons.add(new Contact(AccountId = acc.Id, FirstName = 'Sam', 
                             LastName = 'Ple', email='test1@salesforce.com'));
        cons.add(new Contact(AccountId = acc.Id, FirstName = 'Ted', 
                             LastName = 'Test', email='test2@salesforce.com'));
        insert cons;
        
        List<Opportunity> opps = new List<Opportunity>{};
        opps.add(new Opportunity(AccountId = acc.Id, Name = 'Test Opportunity 1', 
                                 StageName='Closed Won' , CloseDate=Date.TODAY()));
        opps.add(new Opportunity(AccountId = acc.Id, Name = 'Test Opportunity 2', 
                                 StageName='Closed Lost', CloseDate=Date.TODAY()));
        insert opps;        
        
        // Initialise a test instance of a clone plus controller.
        ClonePlusController cpc = new ClonePlusController();
        
        // Populate the controller variables as if the Account created above had been 
        // selected for cloning along with all its related contacts.
        cpc.headSobject    = acc;
        
        List<ClonePlusController.RelatedObjectRow> relatedContactRows 
                               = new List<ClonePlusController.RelatedObjectRow>{};
        relatedContactRows.add(new ClonePlusController.RelatedObjectRow(cons[0]));
        relatedContactRows.add(new ClonePlusController.RelatedObjectRow(cons[1])); 
        
        List<ClonePlusController.RelatedObjectRow> relatedOpportunityRows 
                               = new List<ClonePlusController.RelatedObjectRow>{};
        relatedOpportunityRows.add(new ClonePlusController.RelatedObjectRow(opps[0]));
        relatedOpportunityRows.add(new ClonePlusController.RelatedObjectRow(opps[1]));
        
        // Deselect one of the Opportunities, indicating that it should not be cloned.       
        relatedOpportunityRows[0].selected = false;
        
        cpc.objectChildren = new List<ClonePlusController.RelatedObjects>{};
        cpc.objectChildren.add(new ClonePlusController.RelatedObjects(relatedContactRows,    
                                                                      'Contacts', 'AccountId'));
        cpc.objectChildren.add(new ClonePlusController.RelatedObjects(relatedOpportunityRows,
                                                                      'Opportunity', 'AccountId'));
        
        // Call the do clone method. Record the page reference passed in response. 
        // We will use this later in the test to ensure that the user is correctly being 
        // directed to the new cloned object detail page.
        PageReference doCloneResult = cpc.doClone();
        
        // Check the database for the presence of the newly cloned records.
        List<Account> clonedAccs = [SELECT Name, Phone,
                                          (SELECT FirstName, LastName, Email
                                           FROM Contacts
                                           ORDER By FirstName),
                                           (SELECT Name, StageName, CloseDate
                                           FROM Opportunities
                                           ORDER By Name)
                                    FROM Account
                                    WHERE Id != :acc.Id];
        
        system.assertEquals(1                , clonedAccs.size());
        system.assertEquals(acc.Name         , clonedAccs[0].Name);
        system.assertEquals(acc.Phone        , clonedAccs[0].Phone);
        
        //Both of the selected contacts should have been cloned.
        system.assertEquals(2                , clonedAccs[0].Contacts.Size());
        system.assertEquals(cons[0].FirstName, clonedAccs[0].Contacts[0].FirstName);
        system.assertEquals(cons[0].LastName , clonedAccs[0].Contacts[0].LastName);
        system.assertEquals(cons[0].email    , clonedAccs[0].Contacts[0].Email);
        system.assertEquals(cons[1].FirstName, clonedAccs[0].Contacts[1].FirstName);
        system.assertEquals(cons[1].LastName , clonedAccs[0].Contacts[1].LastName);
        system.assertEquals(cons[1].email    , clonedAccs[0].Contacts[1].Email);
        
        // Only 1 opportunity should have been cloned.
        system.assertEquals(1                , clonedAccs[0].Opportunities.Size());
        system.assertEquals(opps[1].Name     , clonedAccs[0].Opportunities[0].Name);
        system.assertEquals(opps[1].StageName, clonedAccs[0].Opportunities[0].StageName);
        system.assertEquals(opps[1].CloseDate, clonedAccs[0].Opportunities[0].CloseDate);
        
        // Finally, verify that the page reference returned by the clone method is the 
        // view page of the new account object.
        system.assertEquals('/' + clonedAccs[0].Id, doCloneResult.getURL());
    }

    /*
        Test the "doClone" method. This method clones the parent object held in the controller,
        and all the related objects that have also been selected for copying.

        Initialise an instance of the clone plus controller, and don't populate the class variables with
        some sample objects. This will cause an error when calling the do clone method, 
        as there is nothing to clone. 
  
        Instead of redirecting the user, a page message should be added to the current page, 
        informing the user of the error that has occured.
    */
    private static testmethod void testDoCloneFailure()
    {   
        // Initialise a test instance of a clone plus controller.
        ClonePlusController cpc = new ClonePlusController();
        
        // Call the do clone method. Record the page reference passed in response. 
        PageReference doCloneResult = cpc.doClone();
        
        // Verify that that a null page reference is returned by the method call, 
        // and that a page message has been generated to inform the user a fault has occured.
        system.assertEquals(1, ApexPages.getMessages().size());
    }
    
    /*
     When the related object rows have been populated, the getId method can be used to interrogate
     the generic sobject and retrieve the id field of the object.
      
     However, if no id field is visible to the user, then this should not cause an exception to occur.
     Instead, a blank value should be returned.
    */
    private static testmethod void testGetObjectIdException()
    {
     // Start the test by intialising some object rows in the controller. 
     // One object row will not contain an object, resulting in a (caught) exception 
     // when the id is requested.
     Contact testSavedContact = new Contact(FirstName = 'Sam', LastName = 'Ple', 
                                            email='test1@salesforce.com');
     insert testSavedContact;
     
     ClonePlusController.RelatedObjectRow testSavedContactRow 
                         = new ClonePlusController.RelatedObjectRow(testSavedContact);
     ClonePlusController.RelatedObjectRow testNullObjectRow   
                         = new ClonePlusController.RelatedObjectRow(null);
     
     System.assertEquals(testSavedContact.Id, testSavedContactRow.getId());
     System.assertEquals(''                 , testNullObjectRow.getId());
    }
}


My advice to all those using the test class (especially who have previously asked about testing), is that you should use this as a base for building your own test methods. Substitute the test Account -> Contact and Account -> Opportunities relationships used in the test for the objects you are going to clone in your own org.

One last thought, although the minimum coverage is 75%, I have built the test class so that the coverage reaches 100%. As a general rule of thumb I always intend to write complete coverage of code, as the more coverage you have, the more likely you are to catch the introduction of a bug in your system after some new development, or a new release.