0

I have the LightningSelfRegisterController and LightningSelfRegisterControllerTest that were prebuilt in SF. The test only covers the class at something like 72%. I need to get this to 90% and am focusing on the following code that isn't covered:

CLASS

global class LightningSelfRegisterController {

< ... bunch of code that works and is covered ...>

    @AuraEnabled
    public static List<Map<String,Object>> getExtraFields(String extraFieldsFieldSet) { 
        List<Map<String,Object>> extraFields = new List<Map<String,Object>>();
        Schema.FieldSet fieldSet = Schema.SObjectType.User.fieldSets.getMap().get(extraFieldsFieldSet);
        system.debug('extraFieldsFieldSet is '+extraFieldsFieldSet);
        system.debug('fieldSet is ' +fieldSet);
        //if(!Test.isRunningTest()) { Removed not running test so we can cover the following lines
        if (fieldSet != null) {
            for (Schema.FieldSetMember f : fieldSet.getFields()) {
                Map<String, Object> fieldDetail = new Map<String, Object>();
                fieldDetail.put('dbRequired', f.getDBRequired());
                fieldDetail.put('fieldPath', f.getFieldPath());
                fieldDetail.put('label', f.getLabel());
                fieldDetail.put('required', f.getRequired());
                fieldDetail.put('type', f.getType());
                fieldDetail.put('value', '');   // client will populate
                extraFields.add(fieldDetail);
            }}//} - Removed not running test above
        return extraFields;
    }
}

TEST CLASS

@IsTest
public without sharing class LightningSelfRegisterControllerTest {
< ... bunch of code that works and covers the class ...>

    @IsTest
    static void testGetExtraExtraFields() {
        
                
        MetadataService.MetadataPort service = createService();
        
        // FieldSet
        MetadataService.FieldSet fieldSet = new MetadataService.FieldSet();
        fieldSet.fullName = 'Test__c.MyFieldSet';
        fieldSet.label = 'My FieldSet';
        fieldSet.description = 'Used by my VF page';
        MetadataService.FieldSetItem myAvailableField = new MetadataService.FieldSetItem();
        myAvailableField.field = 'TestField__c';
        myAvailableField.isFieldManaged = true;
        myAvailableField.isRequired = true;
        fieldSet.availableFields = new List<MetadataService.FieldSetItem>();
        fieldSet.availableFields.add(myAvailableField);

        // Create
        MetadataService.AsyncResult[] results = service.create(new List<MetadataService.Metadata> { fieldSet });
        
        /*List < Map < String, Object >> fieldlist = new List < Map < String, Object >> ();
        Map < String, String > paramsMap = initializeParams();
        Map < String, Object > fieldMap = new Map < String, Object > ();
        fieldMap.put('description', 'new field');
        fieldMap.put('fieldPath', 'dummyPath');
        fieldlist.add(fieldMap);*/
        String extraFields = fieldset.fullName;
        
        //String extraFields = JSON.serialize(fieldset);
        System.assertEquals(new List < Map < String, Object >> (), LightningSelfRegisterController.getExtraFields(extraFields));
    }
}

I tried implementing the method of creating the fieldset using the MetadaService class based on Can we insert or update FieldSets using apex code?. I had to add some bits of the MetadataService class that were not present in our version -- grabbed from https://github.com/financialforcedev/apex-mdapi/blob/master/apex-mdapi/src/classes/MetadataServiceExamples.cls#L492.

I know it would be better to use the fieldset from the VF Page or component using this class, but we are not currently using this code and need it for future use.

Currently the code above runs without error. The problem is that it doesn't wind up covering that code section. When I system.debug the value in the class for "extraFieldsFieldSet" I get "Test__c.MyFieldSet". But when I system.debug the value for "fieldSet" I get null.

What exactly is

Schema.SObjectType.User.fieldSets.getMap().get();

actually looking for? If it is the fieldSet.FullName value, that is what I am passing. But is that the right thing?

EDIT

Now my test code has incorporated more of the MetadataService which got it to work to the level of getting a new error message saying that the TestMethod cannot make a Web Callout. So I inserted a mock web service. Which worked in getting rid of that error message, but now I have a new error message "attempt to dereference a null object." from the line:

handleSaveResults(results[0]);

So it looks like the attempt to create the metadata fieldset in the line above is producing no results. Here is what my code now looks like for the test class:

        @IsTest
    static void testGetExtraExtraFields() {
        
        System.Test.setMock(WebServiceMock.class, new WebServiceMockImpl());
        Test.startTest();
            MetadataService.MetadataPort service = createService();
        
        
        // FieldSet
        MetadataService.FieldSet fieldSet = new MetadataService.FieldSet();
        
        fieldSet.fullName = 'Test__c.MyFieldSet';
        fieldSet.label = 'My FieldSet';
        fieldSet.description = 'Used by my VF page';
        MetadataService.FieldSetItem myAvailableField = new MetadataService.FieldSetItem();
        myAvailableField.field = 'TestField__c';
        myAvailableField.isFieldManaged = true;
        myAvailableField.isRequired = true;
        fieldSet.availableFields = new List<MetadataService.FieldSetItem>();
        fieldSet.availableFields.add(myAvailableField);

        // Create
        List<MetadataService.SaveResult> results =
            service.createMetadata(
                new MetadataService.Metadata[] { fieldSet });
        handleSaveResults(results[0]);
        
        System.assertEquals(new List < Map < String, Object >> (), LightningSelfRegisterController.getExtraFields(fieldset.fullName));
        Test.stopTest();
    }
    
    private class WebServiceMockImpl implements WebServiceMock 
    {
        public void doInvoke(
            Object stub, Object request, Map<String, Object> response,
            String endpoint, String soapAction, String requestName,
            String responseNS, String responseName, String responseType) 
        {
            if(request instanceof MetadataService.retrieve_element)
                response.put('response_x', new MetadataService.retrieveResponse_element());
            else if(request instanceof MetadataService.checkDeployStatus_element)
                response.put('response_x', new MetadataService.checkDeployStatusResponse_element());
            else if(request instanceof MetadataService.listMetadata_element)
                response.put('response_x', new MetadataService.listMetadataResponse_element());
            else if(request instanceof MetadataService.checkRetrieveStatus_element)
                response.put('response_x', new MetadataService.checkRetrieveStatusResponse_element());
            else if(request instanceof MetadataService.describeMetadata_element)
                response.put('response_x', new MetadataService.describeMetadataResponse_element());
            else if(request instanceof MetadataService.deploy_element)
                response.put('response_x', new MetadataService.deployResponse_element());
            else if(request instanceof MetadataService.updateMetadata_element)
                response.put('response_x', new MetadataService.updateMetadataResponse_element());
            else if(request instanceof MetadataService.renameMetadata_element)
                response.put('response_x', new MetadataService.renameMetadataResponse_element());
            else if(request instanceof  MetadataService.cancelDeploy_element)
                response.put('response_x', new MetadataService.cancelDeployResponse_element());
            else if(request instanceof  MetadataService.deleteMetadata_element)
                response.put('response_x', new MetadataService.deleteMetadataResponse_element());
            else if(request instanceof  MetadataService.upsertMetadata_element)
                response.put('response_x', new MetadataService.upsertMetadataResponse_element());
            else if(request instanceof  MetadataService.createMetadata_element)
                response.put('response_x', new MetadataService.createMetadataResponse_element());
            else if(request instanceof  MetadataService.deployRecentValidation_element)
                response.put('response_x', new MetadataService.deployRecentValidationResponse_element());
            else if(request instanceof MetadataService.describeValueType_element)
                response.put('response_x', new MetadataService.describeValueTypeResponse_element());
            else if(request instanceof MetadataService.checkRetrieveStatus_element)
                response.put('response_x', new MetadataService.checkRetrieveStatusResponse_element());
            return;
        }
    }  
    
    public static void handleSaveResults(MetadataService.SaveResult saveResult)
    {
        // Nothing to see?
        if(saveResult==null || saveResult.success)
            return;
        // Construct error message and throw an exception
        if(saveResult.errors!=null)
        {
            List<String> messages = new List<String>();
            messages.add(
                (saveResult.errors.size()==1 ? 'Error ' : 'Errors ') +
                    'occured processing component ' + saveResult.fullName + '.');
            for(MetadataService.Error error : saveResult.errors)
                messages.add(
                    error.message + ' (' + error.statusCode + ').' +
                    ( error.fields!=null && error.fields.size()>0 ?
                        ' Fields ' + String.join(error.fields, ',') + '.' : '' ) );
            if(messages.size()>0)
                throw new MetadataServiceExamplesException(String.join(messages, ' '));
        }
        if(!saveResult.success)
            throw new MetadataServiceExamplesException('Request failed with no specified error.');
    }
    
        public class MetadataServiceExamplesException extends Exception { }
}
3
  • Dependency injection is your friend here too.
    – Adrian Larson
    Commented Apr 4, 2023 at 0:50
  • Thought you might say that. I feel like I'm a chef who can follow a recipe, maybe even make substitutions if I am out of an ingredient. But adding new ingredients to create a new flavor profile is still a little beyond me. I just don't understand the steps to creating a dependency injection. I can follow what you all are saying when I see the answer but still having trouble coming up with the answer myself. I tried going down the DI route but got flustered and so tried the MetaData service route. I have been reading on DI as well, but ... Do you have a good step-by-step guide to creating a DI? Commented Apr 4, 2023 at 6:12
  • I am attempting to use dependency injection based on the response here: salesforce.stackexchange.com/questions/14590/…. However, I can't figure out where to put this line "public fieldSet List<IFieldSetMember>" or what to replace? Commented Apr 4, 2023 at 18:15

1 Answer 1

1

Above dependency injection, you may wish to simply use a real field set. To do so, you need to reorganize the code into slightly smaller chunks. This strategy is a common approach to simplify testing.

public static List<Map<String, Object>> getDetails(FieldSet fieldSet)
{
    // implementation here
}

Note the return type List<Map<String, Object>> which is I consider a code smell. I call it a loosely typed data structure, though perhaps "weakly typed" would be more correct from a computer science perspective. Regardless, it is an anti-pattern in UI back end design, and the converse (which is desirable) is what I call concretely typed data (perhaps "strongly typed" for the CS inclined).

What that means is, you use Object Oriented Programming and define a model class to represent this data structure. In this case, I would actually make a two tier setup of such models. Make a top-level FieldSet class, and inside it, a FieldSetMember class. Most of the details you should be able to fill in yourself. Basic organization will likely look something like below.

public virtual with sharing class FieldSet
{
    public virtual List<Member> fields { get; private set; }
    public FieldSet(Schema.FieldSet fieldSet)
    {
        // populate from the real deal
    }
    public virtual class Member
    {
        public Member(FieldSetMember member)
        {
            // populate from the real deal
        }
    }
}

Note I only use Schema. to clarify the namespace for FieldSet since the top-level class shadows that name. Some find that inadvisable and might name it something else, but I prefer clean simplicity where I can have it. Anyway, you almost never need the Schema. prefix.

I digress. The crux of how it helps you is: how do you enable injection? There are a number of paths from here, but one might look like the below.

public virtual with sharing class FieldSet
{
    @TestVisible static void type = FieldSet.class;
    public static FieldSet parse(Schema.FieldSet fieldSet)
    {
        return type.newInstance().parse(fieldSet);
    }
    public List<Member> { get; private set; }
    public FieldSet()
    {
        members = new List<Member>();
    }
    public FieldSet parse(Schema.FieldSet fieldSet)
    {
        //f ormer implementation
        return this;
    }
    // member implementation unchanged
}

Now in your tests, you can define a different type which just constructs whatever members you please. You can also mock the members if you find that necessary.

@IsTest static void testMyMethod()
{
    FieldSet.type = MockFieldSet.class;
    
    Test.startTest();
        FieldSet mock = FieldSet.parse(/*some fieldset*/);
    Test.stopTest();
    
    // assert what you wish
}
class MockFieldSet
{
    MockFieldSet(List<String> fieldNames)
    {
        fields = new List<FieldSet.Member>();
        for (String fieldPath: fieldPaths)
            fields.add(new MockField(fieldPath));
    }
}
class MockField
{
    MockField(String fieldPath)
    {
        this.fieldPath = fieldPath;
        // set some attributes
    }
}

Using this kind of proxy, you can rigorously test how you parse field sets into this custom model, then inject different models into your tests.

6
  • Adrian, thanks for the response. Maybe I misunderstood, but the point was I didn't have a fieldset set up to begin with and didn't want to hard code it in case new fieldsets were created in different orgs. This is part of the prebuilt Lightning Self Reg process including the Aura component that allows you to add any fieldset you created to the registration process to collect the extra fields. Or perhaps you are saying that there is some other way to code in Apex to pass "FieldSet.parse(/*some fieldset*/);"? Commented Apr 5, 2023 at 22:38
  • Aside from that I am not sure how to incorporate the getExtraFields method called by the Aura Component. Commented Apr 5, 2023 at 22:38
  • When testing the controller, yes you just inject a different type for the FieldSet to construct, and have that extending type mock the fields also. As for testing the functionality, I would just create a dummy field set if none exist. You need a real one somewhere to exist.
    – Adrian Larson
    Commented Apr 6, 2023 at 0:04
  • This article says it is possible to create a fieldset using MetadataService. Is that not true? Commented Apr 6, 2023 at 16:20
  • Not in a unit test.
    – Adrian Larson
    Commented Apr 6, 2023 at 16:30

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .