11

Looking for some help/advice in writing a test class. The class I need to test is an apex class to schedule a report that needs to be sent to an external email address once a month as a .csv attachment. I was able to build the test class using some advice from an old post on the dev community; I found the solution in TESTOURSFDC's comment to work sufficiently for us. Copy of my code below.

What I'm struggling with is how to test that an email has been sent. The SF documentation for scheduled apex classes are all built with regular SF maintenance use cases (e.g. create tasks on opps that fulfill xyz criteria once a month), so their test classes are easy to re-create (query for tasks with related WhatIds). I've never worked with apex to send emails before, so I'm admittedly quite unfamiliar with the methods and metadata available.

Has anyone tested something similar to this that could help me out?

Thanks!!

Class (updated):

global class reportExporter implements System.Schedulable {

    global void execute(SchedulableContext sc) {

        ApexPages.PageReference report = new ApexPages.PageReference('/00OXXXXXXXXXXXX?csv=1');

        Messaging.EmailFileAttachment attachment = new Messaging.EmailFileAttachment();

        attachment.setFileName('Report.csv');

        //use getContent() only if not a test, otherwise test will fail
        //http://salesforce.stackexchange.com/questions/97223/test-fails-because-of-testmethod-do-not-support-getcontent-call
        if(!test.isRunningTest()){
            attachment.setBody(Blob.valueof(report.getContent().toString()));   
        } else {
            attachment.setBody(Blob.valueof('TEST BODY'));
        }

        attachment.setContentType('text/csv');

        Messaging.SingleEmailMessage message = new Messaging.SingleEmailMessage();

        message.setFileAttachments(new Messaging.EmailFileAttachment[] { attachment } );

        message.setSubject('Report');

        message.setPlainTextBody('The report is attached.');

        message.setToAddresses( new String[] { 'name@example.com' } );

        message.setBccAddresses( new String[] { 'name@example.com' } );

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

        List<Messaging.SendEmailResult> results = Messaging.SendEmail(new Messaging.Email[] { message });
            System.debug(results);
        if (!results.get(0).isSuccess()) {
            System.StatusCode statusCode = results.get(0).getErrors()[0].getStatusCode();
            String errorMessage = results.get(0).getErrors()[0].getMessage();

        }

    }

}

Test Class:

@isTest
private class testReportExporter {

   // CRON expression: midnight on March 15 2022
   // Because this is a test, job executes immediately after Test.stopTest() regardless of date
   public static String CRON_EXP = '0 0 0 15 3 ? 2022';

   static testmethod void test() {
      Test.startTest();

      reportExporter re = new reportExporter();

      // Schedule the test job
      String jobId = System.schedule('ScheduleApexClassTest',
                        CRON_EXP, re);

      // Get the information from the CronTrigger API object
      CronTrigger ct = [SELECT Id, CronExpression, TimesTriggered, 
         NextFireTime
         FROM CronTrigger WHERE id = :jobId];

      // Verify the expressions are the same
      System.assertEquals(CRON_EXP, 
         ct.CronExpression);

      // Verify the job has not run
      System.assertEquals(0, ct.TimesTriggered);

      // Verify the next time the job will run
      System.assertEquals('2022-03-15 00:00:00', 
         String.valueOf(ct.NextFireTime));

      // Verify the scheduled job hasn't run yet.
      Integer beforeInvocations = Limits.getEmailInvocations();
      System.assertEquals(0,beforeInvocations,'no email sent');

      //Verify class results are not false
      System.debug(re);

      Test.stopTest();
}
}
  • 1
    Email is DML. You should be able to assert the DML operation at the end of your test method. Remember that in a schedulable, it's after the test.stopTest that the class executes and you'll be able to query for all of your results. – crmprogdev May 27 '16 at 21:19
  • Interesting, I hadn't observed that before. You can check a more specific limit though. @crm – Adrian Larson May 27 '16 at 22:55
  • @AdrianLarson Yes, I saw your answer. I'm the one who gave it the 1st upvote (the only upvote at present in fact). – crmprogdev May 27 '16 at 22:59
  • Email really consumes limits fast! Dml, invocations, daily limits, oh my! – Adrian Larson May 27 '16 at 23:01
  • @crmprogdev do you have any advice for how to query the worker class? I know I need to query the Messaging.SendEmailResult list from the above class (edited), but I'm not sure how to call that from the test class. – mlpSFadmin May 31 '16 at 20:13
6

With respect to your edited question, as @AdrianLarson answered, your schedulable class still doesn't run until immediately after Test.stopTest(). Asserting the change in Limits.getEmailInvocations() should be sufficient for your requirements.

That being said, to answer the question you asked me on how to query the Messaging.SendEmailResult, that's something one executes immediately following their SendEmail Method (you'd do this in a non-async class). It isn't something one can query as those methods are run in the context of Sending an email. Querying them wouldn't have any context unless you queryied the WhoId, WhatId, sender, target, Subject, and other fields, etc to narrow the scope. In doing that you'd presumably be querying an Email record that's related to some Object Record rather than the SendEmailEmailResult method.

So, just add the code that @AdrianLarson provided to your test class and you should be all set. Additionally, you'll also want to assert that your scheduled class has run too. You already have the jobId.

After Test.stopTest(), add something like what's below:

 // Get the information from the CronTrigger API object
CronTrigger ct = [SELECT Id, CronExpression, TimesTriggered, 
     NextFireTime
     FROM CronTrigger WHERE id = :jobId];

// Verify the job has run
System.assertEquals(1, ct.TimesTriggered);

// Assert emails have been sent
system.assertEquals(1, invocations,  ' An email should be sent');

Edit

Your chron expression schedules the job for March 15th, 2022 at midnight which should work fine. Looking closer at your code, I see several things that could be the cause of your issues:

The first one is minor, but should be corrected. Use Schedulable, not System.Schedulable when you declare your class.

global class reportExporter implements System.Schedulable {

I suspect the real source of your problem is in this section of code below. Adding debug statements should reveal if that's the case. It would seem that either more of this code should be wrapped in if(!test.isRunningTest()) or else you've not created the data you need for it to run.

You begin by calling a page reference, creating the attachment, setting a file name for a blob, declare it's type, etc and actually attach it. It would seem to me that you'd need to create a blob in your test class called 'TEST BODY', but I don't see that in your test method.

    Messaging.EmailFileAttachment attachment = new Messaging.EmailFileAttachment();

    attachment.setFileName('Cost Plus Merchants.csv');

    //use getContent() only if not a test, otherwise test will fail
    //http://salesforce.stackexchange.com/questions/97223/test-fails-because-of-testmethod-do-not-support-getcontent-call
    if(!test.isRunningTest()){
        attachment.setBody(Blob.valueof(report.getContent().toString()));   
    } else {
        attachment.setBody(Blob.valueof('TEST BODY'));
    }

    attachment.setContentType('text/csv');

    Messaging.SingleEmailMessage message = new Messaging.SingleEmailMessage();

    message.setFileAttachments(new Messaging.EmailFileAttachment[] { attachment } );
  • I appreciate the clarification on the query, that's helpful. As for those lines of code, I have to repeat what I told Adrian -- I tried adding them, but the asserts both fail. If I debug, that CronTrigger query returns the same results pre- and post- Test.stopTest(). Is there a trick to getting the schedulable class to run? Everything seems to suggest it happens automatically after Test.stopTest() I appreciate all your help!! – mlpSFadmin Jun 1 '16 at 20:15
  • See the edits to my answer. – crmprogdev Jun 1 '16 at 21:13
21

You can use the Limits.getEmailInvocations method .

Test.startTest();
    methodThatSendsEmail();
    Integer invocations = Limits.getEmailInvocations();
Test.stopTest();

system.assertEquals(1, invocations, 'An email should be sent');

What to do about NO_MASS_MAIL_PERMISSION?

You can use the Messaging.sendEmail method with the optional allOrNone parameter set to false. To ignore the failures would be...unwise. I would create a custom object called Email_Log__c and save all results. I advise you to track anything you can set on the SingleEmailMessage class.

The below is a simple outline of how you can approach this problem.

public static final String DELIMITER = ';';
public static void safeSend(List<Messaging.SingleEmailMessage> emails)
{
    Integer index = 0;
    List<Email_Log__c> logs = new List<Email_Log__c>();
    for (Messaging.SendEmailResult result : Messaging.sendEmail(emails, false))
    {
        logs.add(buildLog(result, emails[index++]));
    }
    insert logs;
    // proper error handling omitted for brevity
}
static Email_Log__c buildLog
    (Messaging.SingleEmailMessage email, Messaging.SendEmailResult result)
{
    return new Email_Log__c(
        Is_Success__c = result.isSuccess(),
        Error_Messages__c = concatenateErrors(result.getErrors()),
        To_Addresses__c = String.join(email.getToAddresses(), DELIMITER),
        Html_Body__c = email.getHtmlBody()
        // other properties
    );
}
static String concatenateErrors(List<Messaging.SendEmailError> errors)
{
    List<String> messages = new List<String>();
    for (Messaging.SendEmailError error : errors) messages.add(error.getMessage());
    return String.join(messages, DELIMITER);
}

This approach is even more accessible via testing, because you can assert on the log objects created in addition to invocations. Even if every result is a failure, the invocation count augments by one.

system.assertEquals(1, invocations, 'An email should be sent');

List<Email_Log__c> logs = [SELECT To_Addresses__c, Etc__c FROM Email_Log__c];
system.assertEquals(numberOfEmailsSent, logs.size(), 'Each email should be sent');
for (Email_Log__c log : logs)
{
    // any additional assertions you desire
}
  • @crmprogdev - as long as we're talking email, there's a builder pattern (fluent) sendEmail example here that I adapted/enhanced and to test, used the Limits method to test error handling (like insufficient capacity). I think Adrian may have been the one who taught me this some thousand answers back – cropredy May 28 '16 at 1:46
  • Hrm, I like this idea, but I'm having difficulty getting it to work. In particular, I'm getting NO_MASS_MAIL_PERMISSION errors because my sandbox is typically set to system emails only. I feel like adjusting email deliverability any time I need to run this test is a clunky solution, no? – mlpSFadmin May 31 '16 at 19:03
  • @Megan You have updated your post since posting this question, is it your only remaining question? Your updates make no mention of this issue so it is difficult to decipher your intentions. – Adrian Larson May 31 '16 at 20:54
  • @AdrianLarson updated to include what I have right now as a test class, as I thought that would be helpful to see. Questions still remains how to write a better class though -- that test class above 1. doesn't really test the class (I don't know how to assert without fail after Test.stopTest) and 2. fails as soon as I change sandbox Email Deliverability from All Email to System Emails Only (norm for us, and desired). If you see my comment above, I'm pretty sure I have to query the Messaging.SendEmailResult from the worker class, but I'm stumped as to how. – mlpSFadmin May 31 '16 at 22:21
  • I suppose I could do what you're suggesting with the invocations method AFTER the Test.stopTest as well as before, but every time I tried that it failed, and I'm not sure why. – mlpSFadmin May 31 '16 at 22:23

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.