1

Currently looking at the Using Dynamic References with Custom Objects and Packages tutorial.

For reasons I won't speculate on, step 7 included the controller extension code (again) instead of the unit test code, so I took it as a challenge to myself to write the unit tests, after doing a little refactoring of the controller extension itself.

So, now the extension looks like this:

public virtual class VF_BookVFCtrlExt 
    {
        @testVisible private ApexPages.StandardController standardController;
        @testVisible private Set<String>                  bookFieldnameSet  = new Set<String>();
        @testVisible private List<String>                 availableFieldList;

        public VF_BookVFCtrlExt (ApexPages.StandardController standardController)
        {
            this.standardController = standardController;
            buildBookFieldNameSet(Schema.SObjectType.Book__c.fields.getMap());

            if (!bookFieldnameSet.isEmpty()) { addFieldsToController(bookFieldnameSet, false); }
        }

        @testVisible private void buildBookFieldNameSet(Map<String, Schema.SObjectField> fieldByNameMap)
        {
            for (String fieldName : fieldByNameMap.keySet())
            {
                Schema.DescribeFieldResult field = fieldByNameMap.get(fieldName).getDescribe();

                // Only include accessible fields
                if (field.isAccessible() && field.isCustom()) 
                { 
                    bookFieldnameSet.add(fieldName); 
                }           
            }
        }

        public List<String> getAvailableFieldList()
        {
                addFieldsToController(bookFieldnameSet, true);
                return new List<String>(bookFieldnameSet);
        }

        // ----------------------------------------------------------------------------------------------------------

        private virtual void addFieldsToController(Set<String> fieldNameSet, Boolean needsReset)
        {
             if (needsReset) { standardController.reset(); }

             standardController.addFields(new List<String>(fieldNameSet));
        }   

    }

And my unit tests look like this:

@isTest
public class VF_BookVFCtrlExtTest 
{
    private static final User TEST_RUN_USER = TEST_DummyUserFactory.createDefaultTestRunUser();

    @isTest private static void testAvailableFieldList()
    {
         // GIVEN
         Book__c dummyBook = TEST_DummyBookFactory.create(TEST_DummyInsertModeEnum.REAL);
         VF_BookVFCtrlExt targetVFControllerExtension = createBookVFCtrlExt(dummyBook); 

         // WHEN
         List<String> resultFieldList = null;
         System.runAs(TEST_RUN_USER)
         {
            Test.startTest();
            { 
                resultFieldList = targetVFControllerExtension.getAvailableFieldList(); 
            }
            Test.stopTest();
         }

         // THEN
         List<String> expectedStringList = new List<String>{ 'isbn__c', 'author__c', 'title__c', 'price__c', 'publisher__c' };
         TEST_AssertionHelper.containsSame(expectedStringList, resultFieldList);
    }

    // ---------------------------------------------------------------------------------

    @isTest private static void testBuildBookFieldNameSet()
    {
         // GIVEN
         Map<String, Schema.SObjectField> dummyFieldByNameMap = Schema.SObjectType.Book__c.fields.getMap();              

         Book__c dummyBook = TEST_DummyBookFactory.create(TEST_DummyInsertModeEnum.REAL);
         VF_BookVFCtrlExt targetVFControllerExtension = createBookVFCtrlExt(dummyBook); 

         // WHEN
         System.runAs(TEST_RUN_USER)
         {
            Test.startTest();
            {
                targetVFControllerExtension.buildBookFieldNameSet(dummyFieldByNameMap); 
            }
            Test.stopTest(); 
         }

         // THEN
         Set<String> resultBookFieldNameSet = targetVFControllerExtension.bookFieldnameSet;
         List<String> expectedStringList = new List<String>{ 'isbn__c', 'author__c', 'title__c', 'price__c', 'publisher__c' };
         TEST_AssertionHelper.containsSame(expectedStringList, new List<String>(resultBookFieldNameSet));
    }

    // ---------------------------------------------------------------------------------

    private static VF_BookVFCtrlExt createBookVFCtrlExt(Book__c dummyBook)
    {
        PageReference pageReference = Page.VF_BookView;
        Test.setCurrentPage(pageReference);
        ApexPages.currentPage().getParameters().put('id', dummyBook.id);
        ApexPages.StandardController dummyStandardController = new ApexPages.StandardController(dummyBook); 
        return new MockBookVFCtrlExt(dummyStandardController);
    }

    // -----------------------------------------------------------------------------------------------------

    private class MockBookVFCtrlExt extends VF_BookVFCtrlExt
    {
        public MockBookVFCtrlExt(ApexPages.StandardController standardController) 
        {
            super(standardController);  
        }

        private virtual void addFieldsToController(Set<String> fieldNameSet, Boolean needsReset)
        {
            // Skip these methods for test.
        }
    }
}

... which bring me up to 83% coverage.

Uncovered, specifically, is the virtual addFieldsToController() method, which is expected because I mocked this methods. But I mocked this method because the StandardController methods were causing exceptions like:

  • System.SObject Exception: You cannot call addFields after you've already loaded the data.
  • System.SObject Exception: You cannot call reset when the data is being passed into the controller by the caller.

I realize, by some standards, 83% coverage is "good enough" for deploy, but as a maximalist, I'd like to provide coverage for this virtual method as well.

3

On the addFields call, I've used the simpler pattern of:

public MyController(ApexPages.StandardController sc) {
    if (!Test.isRunningTest()) {
        sc.addFields(new String[] {...});
    }
    ...
}

which might squeeze a line or two more of coverage. The core problem is that the platform forces you to add code to allow testing which is not good.

On the subject of coverage, in quite large code bases we target and achieve 90% overall, with some classes at 100% and some at much lower percentages. Our aim is to test the things that can break and aiming much higher doesn't provide a good return for the effort IMHO.

| improve this answer | |
  • thanks for the response. I considered this possibility, but I think mocking allows for cleaner code. With your solution, we'd still have those lines uncovered. True 100% is overkill, but it would still be good to know how to test it (if possible) rather than just how to get around it. – Brian Kessler Nov 18 '14 at 13:39
  • also consider voting for this idea: success.salesforce.com/ideaView?id=08730000000YmzmAAC – Guy Clairbois Mar 10 '16 at 14:23
  • 100% Test Coverage is always possible with a cost: well-written code and / or Transaction Resources (queries, etc.). Instead of calling sc.addFields(...), query the controller's Record – Scott Pelak Oct 17 '16 at 21:33
  • Using of Test.isRunningTest is the very last thing you should do in test methods. Just list all required fields on a visualforce page (in hidden area) to let standard controller know about these fields. – Yury Bondarau Nov 8 '16 at 17:46
  • @Yury Agreed your approach is an alternative. Personally I prefer the cohesion of fields used by the controller class being referenced in the controller class. Pushing the fields out into the page is also pretty pointless. – Keith C Nov 8 '16 at 17:55
3

There is another solution, see:

http://raydehler.com/cloud/clod/best-practice-related-fields-with-standard-controller-extensions.html

You don’t have to include a field in StandardController.addFields() to get it to be included and avoid that pesky SObject row was retrieved via SOQL without querying error. Simply include it in the visualforce page as an <apex:outputField>! Check out this snippet, which resolves the error in question:

<apex:page standardController="Contact" extensions="ContactExt">
<apex:outputField value="{!contact.Account.BillingCity}" />
City: {!city}
</apex:page>

public class ContactExt {
    public Contact record {get; set;}
    public String city {
        get {
            return record.Account.BillingCity;
        }
        private set;
    }

    public ContactExt(ApexPages.StandardController sc) {
        record = (Contact)sc.getRecord();
    }
}

No error! You don’t even need to use the outputField in your page, you can include rendered=”false” — but you do need to have it merge there to have the field pulled into getRecord().

| improve this answer | |
  • 1
    this solution wouldn't actually work for the askers solutions because his page selects fields dynamically – NSjonas May 26 '16 at 2:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.