2

I am having some issues with writing a test class for my trigger and helper class, which sends an email and .pdf attachment to a contact specified in the custom setting.

The trigger and helper class produce the desired result in sandbox, but it keeps failing validation when deploying to Production.

There doesn't seem to be a huge amount of documentation on how to write such a test class, but it seems that one cannot send the email from the test class but should rather 'mock' it and retrieve the result?

Any direction would be really helpful. Previously, I was hitting this error 'REQUIRED_FIELD_MISSING, Missing target address (target, to, cc, bcc): []' with my test class at the time, which just wasn't working at all.

This is my trigger:

/*===============================
  * Purpose: The X Team need to receive emails with Data Schedule attachments whenever a Data Schedule attachment has been created against an Opportunity. 
  * 1) This trigger evaluates all Opportunities that have been updated and which have the number of attachments changed and have a number of attachments >=1 and number of data schedules >=1. 
  * 2) Then, all the Attachments which are related to such Opportunities are selected, which are also .pdf attachments only and were created in the last few minutes. 
  * 3) Then, these Attachments are attached to a single email, which leverages an Email Template, and sent to the required recipients. 
  =================================*/

/*===============================
  * Design: Important notes about the design of this trigger:
  * 1) This trigger leverages the Messaging.SingleEmailMessage class because it is NOT possible (as of Q4, 2016) to leverage the Message.MassEmailMessage clas with the .EmailFileAttachment method.
  * 2) This trigger stores the EmailTemplateId and Contact (which may have an email of the distribution list to email mass recipients) in custom settings, so that these can be changed in future easily.
  * 3) The contactId stored in the .setTargetObjectId custom setting is protected by a validation rule (it's very important the contactId specified in the .setTargetObjectId is not deleted).
  * 4) The custom number opportunity fields 'of_Data_Schedules__c' and 'of_Attachments__c' are populated by the Lookup Rollup Summaries. 
  =================================*/

trigger EmailDataScheduleAttachment on Opportunity (after update) {

    List<Id> opportunitiesWithAttachmentsToEmail = new List<Id>(); 

    /* Only evaluate opportunities where there is at least 1 attachment against the opportunity (custom field 'of_attachments__c' populated via lookup rollup summary id = 'a0BD000001ptKZl')
        AND where the custom field 'of_Data_Schedules__c' is >=1 (populated via lookup rollup summary id = 'a0BD000001ptKZl')
        AND where the # of attachments has changed */  
    for (Opportunity opp : Trigger.new) {
        Opportunity oldOpp = Trigger.oldMap.get(opp.Id); 
        if (opp.of_attachments__c >= 1 && opp.of_attachments__c != oldOpp.of_attachments__c && opp.of_Data_Schedules__c >= 1) { 
            opportunitiesWithAttachmentsToEmail.add(opp.Id);  
        }
    }

    // Select the Attachments which have a ParentId of one of the opportunities from above AND which are .pdfs (CPQ writes back in .pdf format, as default) AND which were created 3 minutes ago (to prevent already created Attachments from being re-sent)
    List<Attachment> dataScheduleAttachmentsToEmail = [SELECT Id, ParentId, Name, Body FROM Attachment WHERE ParentId IN : opportunitiesWithAttachmentsToEmail AND ContentType = 'application/pdf' AND CreatedDate > :Datetime.now().addMinutes(-3)]; 

    // Iterate over all the selected opportunities, so that their merge fields can be referenced in the Email template
    for (Integer i=0; i<opportunitiesWithAttachmentsToEmail.size(); i++) {
        if (opportunitiesWithAttachmentsToEmail[i]!=null) { // null pointer check

            EmailUtility.dataScheduleAttachmentSend(opportunitiesWithAttachmentsToEmail[i], dataScheduleAttachmentsToEmail);
        }
    }

}

This is helper class

public class EmailUtility {

        public static void dataScheduleAttachmentSend(Id whatId, List<Attachment> attList) {

            // Instantiate the SingleEmailMessage class to create a single email to be sent
            Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();    

            // Custom Settings

            // .setEmailTemplate 
            setEmailTemplate__c sti = setEmailTemplate__c.getInstance('EmailTemplate1');
            Id emailTemplateId = Id.valueOf(sti.email_template_id__c);

            // .setTargetObjectId 
            setTargetObjectId__c sto = setTargetObjectId__c.getInstance('sto'); 
            Id contactId = Id.valueOf(sto.contactId__c); 

            mail.setReplyTo('abc@aol.com'); 
            mail.setSenderDisplayName('XYZ');
            mail.setBccSender(false);    
            mail.setTemplateId(emailTemplateId);  
            mail.saveAsActivity = false;
            mail.setTargetObjectId(contactId);
            mail.setUseSignature(false);

            List<Messaging.EmailFileAttachment> fileAttachments = new List<Messaging.EmailFileAttachment>(); 
                for (Attachment a : attList) {
                Messaging.EmailFileAttachment efa = new Messaging.EmailFileAttachment(); 
                efa.setFileName(a.Name);
                efa.setBody(a.Body);
                fileAttachments.add(efa);
            }
            mail.setFileAttachments(fileAttachments); 


        Messaging.sendEmail(new Messaging.SingleEmailMessage[] {mail});
        }


    }

This is my current test class

@isTest
public class EmailDataScheduleAttachmentTest {

    /* Create User record */ 
    public static User u = TestHelper.createAdminUser('ahitchings@DataAttachmentScheduleEmailSend');  

    public static testMethod void createSomeTestData() {

        System.runAs(u) {

            // Create & Insert Account record
            Account a = new Account(Name = 'Test Account - Data Schedule Attachments', Company_Profile__c = 'Hedge Fund', Company_Profile_Type__c = 'Buy Side',
                                   Status__c = 'Draft', ownerId = U.Id);
            insert a;

            // Create & Insert Contact record
            Contact c = new Contact(Email = 'ahitchings@TESTDATASCHEDULEabcXYZ.com', LastName = 'HITCHINGS', AccountId = a.Id, OwnerId = u.Id);
            insert c;


            // Create & Insert Opportunity record
            Opportunity opp = new Opportunity(AccountId = a.Id, OwnerId = u.Id, StageName = 'Qualification',  
                                             Name = 'TEST ABC XYZ Opportunity with Attachment and Data Schedule', CurrencyISOCode = 'GBP',
                                             Amount = 500, Billing_Effective_Date__c = Date.today(), CloseDate = Date.today());

            insert opp;

            Test.startTest();

            Opportunity oppJustCreated = [SELECT Id FROM Opportunity LIMIT 1]; 
            oppJustCreated.of_Attachments__c = 1; 
            oppJustCreated.of_Data_Schedules__c = 1; 
            update oppJustCreated;


        }       

        }

    }

Although code coverage for the trigger is 100%, when I run this test class in sandbox, it's returning a stack trace error on the 'update oppJustCreated' line (line 33). The error message itself says:

caused by: System.NullPointerException: Attempt to de-reference a null object Class.EmailUtility.dataScheduleAttachmentSend: line 12, column 1 Trigger.EmailDataScheduleAttachment: line 37, column 1: []

which leads me to think it's do to with the custom settings somehow. In a previous test class, I referenced the custom settings in the createSomeTestData() method, and I was still returning an error which was:

SendEmail failed. First exception on row 0; first error: REQUIRED_FIELD_MISSING, Missing target address (target, to, cc, bcc):

I have four opportunity related test classes in Production which were hitting this error, and it all seemed to be around updating an opportunity.

4

You're on the right track thinking that this has to do with custom settings.

Line 12 in your EmailUtility class is

Id emailTemplateId = Id.valueOf(sti.email_template_id__c);

In unit tests, Custom Setting data is not available unless you use seeAllData=true, so you'll need to insert some data for your custom setting in your test setup.

If you don't insert any data, the result of line 11 will be null, explaining the Null Pointer Exception (NPE) you're running into.

+edit: I was wrong about tests and Messaging.sendEmail, incorrect information has been struck-out.

The next issue that you'd run into is that you aren't allowed to call Messaging.sendEmail from a test context (You'll get an exception if you try). You'll need to wrap that line inside of an if block to prevent your test from attempting to execute it

if(!Test.isRunningTest()){
    // Safe to call Messaging.sendEmail in here
}

Yes, that means that the lines inside of that if block are impossible to get coverage for. That just how things have to be in this case.

After that, you shouldn't see the error about not setting target addresses. It still is something that you should probably look into though. I don't have much experience with using email templates in Apex, but my gut feeling (seeing as you're setting the targetObjectId) is that your email template isn't using merge fields (or using the wrong ones) for the target addresses.

| improve this answer | |
  • 1
    Not true that you can't call Messaging.sendEmail within a test. It's simply about Email Deliverability settings. Also if you have a one line condition, simply checking the if condition (even if you don't run the block) will count as "covered". – Adrian Larson Oct 21 '16 at 17:40
  • Looks like I've conflated a concept or two. Best guess at where that came from would be salesforce.stackexchange.com/questions/54587/… Thanks for the correction! – Derek F Oct 21 '16 at 18:31

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.