0

I have created a apex batch class to create survey invitations, subject records & send the unique survey links to participants. Trying to write a test class to cover it by inserting a surveybut seeing the below error

"DML operation Insert not allowed on Survey"

How to get around this? Can you pls help me with this. Thanks!

//This block inserts survey invitation & survey subject
   if(surveyLifecycle == 'Survey Invitation') {
        Network community= [select Id from Network where Name = 'TestCommunity' LIMIT 1];
        List<Survey> surveyobj = [select Id,name,DeveloperName from survey where name IN: TestSurvey ORDER BY Name ASC]; //Ordering to get 1.English, 2.Spanish
        templates = [select id, Subject, Htmlvalue from EmailTemplate where developername LIKE 'Test_Survey_Invitation%' ORDER BY developername ASC];
        Datetime inviteExpiryDateTime = Datetime.newInstance(Date.Today().year(), Date.Today().month(), 31, 24, 0, 0); //January 31st at 11:59 PM
        for (Opportunity opp : (List<Opportunity>) scope) {
            if (opp.contant__c != null && !processedContactIds.contains(opp.contant__c)) {
                contactIdToNameMap.put(opp.contant__c,opp.contant__r.Name);
                contactIdToSubjectMap.put(opp.contant__c,opp.contant__r.Language__c);
            }
        }
        for(Id conId : contactIdToNameMap.keySet()){
            SurveyInvitation invitation = new SurveyInvitation();
            invitation.CommunityId = community.Id;
            invitation.Name = 'Test Survey'+'_' + System.today().year() +'_'+ contactIdToNameMap.get(conId);
            invitation.OptionsCollectAnonymousResponse = false;
            invitation.OptionsAllowGuestUserResponse = true;
            invitation.OPTIONSALLOWPARTICIPANTACCESSTHEIRRESPONSE = false;
            invitation.SurveyId = surveyobj[0].Id;
            invitation.participantId = conId;
            invitation.InviteExpiryDateTime = inviteExpiryDateTime;
            invitationList.add(invitation);
        }

        if(!invitationList.isEmpty()){
            insert invitationList;
            // Database.SaveResult[] saveResultSurveyInvitList = Database.insert(invitationList, false);
            List<SurveyInvitation> queriedInvitationList = new List<SurveyInvitation>([SELECT Id, Name, SurveyId, Contact.Language__c, ParticipantId, Participant.Name, UUID, InvitationLink FROM SurveyInvitation WHERE Id IN: invitationList]);
            for(SurveyInvitation queriedInvite : queriedInvitationList){
                SurveySubject surObj = new SurveySubject();
                surObj.SubjectId = queriedInvite.ParticipantId;
                surObj.ParentId = queriedInvite.Id;
                surObj.Name = queriedInvite.Name ;
                subjectList.add(surObj);
                
            }
            if(subjectList.isempty())
                insert subjectList;
        }

   } 
   //This block expects an existing survey inviation 
   else if(surveyLifecycle == 'Survey Reminder'){
        templates = [select id, Subject, Htmlvalue from EmailTemplate where developername LIKE 'Survey_Reminder%' ORDER BY developername ASC];
        for(SurveyInvitation surInvite : (List<SurveyInvitation>) scope){
            //some operation
        }
3
  • @Fred - Thank you! Is seealldata=true the only option as I have to insert Survey, Survey Invitation to covert the batch class. What would you suggest?
    – SMorse
    Commented Dec 18, 2023 at 14:30
  • Use the fflib UnitOfWork pattern and fflib Selector pattern to work with mock SObjects - no actual DML is done - but you can verify using apexmocks that a registerNew was done for each insert
    – cropredy
    Commented Dec 18, 2023 at 17:28
  • Updated my code. How do you suggest to cover that considering the error I'm seeing? I don't know if UnitOfWork would fit in test class to allow Survey, Survey Invite, Survey subject records insertion.
    – SMorse
    Commented Dec 18, 2023 at 17:59

1 Answer 1

1

Using selectors and unit of work (fflib) pattern, your code would change to roughly

List<Survey> surveys = SurveysSelector.newInstance().selectByName(new Set<String> {someSurveyName});

...
fflib_ISObjectUnitOfWork uow = Application.UnitOfWork.newInstance();
for (Id conId : contactIdToNameMap.keySet()) {
  SurveyInvitation surveyInvitation = new SurveyInvitation(
       CommunityId = xxx,
       );
  uow.registerNew(surveyInvitation,SurveyInvitation.SurveyId,surveys[0].Id);
  invitationList.add(surveyInvitation);
  SurveySubject surveySubject = new SurveySubject(
     SubjectId = ..);
  uow.registerNew(surveySubject,SurveySubject.ParentId,surveyInvitation);
}
uow.commitWork();

Now, I'm leaving out details here to avoid turning this into an fflib tutorial (see https://www.amazon.com/Salesforce-Platform-Enterprise-Architecture-applications/dp/1804619779). I'm also not replicating your logic completely.

But basically:

  • Selector is used to do queries
  • Unit of Work is defined in Application Class to ensure that SurveyInvitations are inserted before SurveySubject
  • Unit of Work commitWork() does all the magic of linking your parent to child records without having to manage Ids in maps

Once you adopt the pattern, then by using Apex Mocks (see the book for details), you

  • mock a selector to return an in-memory Survey and
  • mock verify that uow.registerNew was called constructing the right fields and linking to your expected related records.

No DML is done at all in the test method (none to setup data - although you could for the participants), and none to actually insert the records - you rely on fflib uow.commitWork to do this correctly

Takeaway

When stuck with unmockable SObjects, a generally reliable pattern to solve this is to

  • wrap the SOQL into a selector object that you can inject at test time a mock implementation that returns in-memory sobjects with mocked Ids.
  • Wrap the DML into the Unit of Work pattern. Then, by mocking the UnitOfWork object, you can verify that the expected DML was "registered" and rely on the UnitOfWork implementation (provided in this example by fflib) to do the DML properly.
2
  • This implementation that I'm working on is time sensitive, so to implement the unitofwork and its related classes, I might have to take this up later. As an alternative, can I have some test data created and use that test data by setting SeeAllDatata = true for the test method I'm working on related to survey & related objectst to only query that test datat? I know it is not always the best practice to use realtime data even though it is a test data & eventually I will incorporate the unitofwork. Does this sound like a plan to resolve this immediately as it is holding up the release.
    – SMorse
    Commented Dec 20, 2023 at 16:21
  • SeeAllData would work but be fragile as it would rely on a Survey existing for the length of time that the testmethod would run (this could be years). I'd make sure you have asserts that verify that the expected Survey exists and if not, tell the test engineer/admin what to do if it doesn't. I wrote the answer I gave to socialize the notion of designs that support dependency injection that, when adopted (perhaps in your next gig), are eminently mockable, better suited to unit testing, even in complex systems , and handle these edge cases of unmockable via DML Sobjects
    – cropredy
    Commented Dec 20, 2023 at 19:09

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