16

I have a batch that sends out an email notification to the triggering user when something went wrong. The code that does this is called from the finish() method.

I'm now looking for an elegant way to test this and could not find a simple solution. The only thing I could think of is create an email service and have my code send emails there if Test.isRunningTest = true.

4 Answers 4

10

what do you actually want to test? the execution of the code or that the message gets delivered?

how about you delegate it to a separate method sendNotication() which you can test independently. furthermore you can test it via console as well

batch class

global class MyBatch implements Database.Batchable<sObject> {

    // BATCH CONTRACT ...

    global void finish(Database.BatchableContext bc){

        if (MY_CONDITION)
        {
            sendNotification();
        }
    }

    public void sendNotification(){

        Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
        // CONFIGURE MAIL

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

}

test class

@isTest
private class MyBatchTest {
    static testMethod void sendNotificationTest(){

      Test.startTest();
      Integer emailbefore = Limits.getEmailInvocations();
      MyBatch batch = new MyBatch();
      MyBatch.sendNotication();

      system.assertNotEquals(emailbefore,Limits.getEmailInvocations(),'should have decreased');
      Test.stopTest();
    }
}
6
  • I want to test if the email is really send and what is received. Commented Jun 21, 2013 at 12:41
  • Counting Email invocations does not work for me. Is this only working when called inside start and stopTest()? I cannot use them as I need to wait for my batch to finish. Commented Jun 21, 2013 at 12:58
  • no, Limits methods normally work every where. the sendNotification sends out a message in any case, you should of course control the actual execution from your finish method. updated the code. maybe I dont get it, but do you want to programatically test the content of the email or just debug it for dev purposes?
    – Seb Wagner
    Commented Jun 21, 2013 at 14:23
  • I find Limits.getEmailInvocations() to be inconclusive towards whether the code works. There are instances where I test the inner most code, and it shows a decrease; I test the outermost code, and it again shows a decrease; but then I test some middle method (uses innermost, but not outermost) and there is no decrease, even when (Messaging.SendEmailResult) result.success == true. Commented Feb 14, 2014 at 9:13
  • 4
    Just a note but as of today while in a test the Limits.getEmailInvocations() call always returns zero (0) so the System.assertNotEquals() code in the above example won't work. Commented Dec 11, 2014 at 18:45
6

In addition to the other answers, there is ApexMocks and the fflib Enterprise Pattern - specifically the Unit of Work Layer

Here's an example:

First, the class/method that sends the email:

public void sendEmail() {
   fflib_ISobjectOfWork uow = Application.UnitOfWork.newInstance();
   Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
   mail.setToAddresses(new list<String> {'[email protected]'};
   mail.setSubject('Greetings, earthlings!');
   ...
   uow.registerEmail(mail); // let UnitOfWork know mail is part of Txn
   uow.commitWork(); // send the mail
}

Of course, if the outbound email is part of a larger transaction, pass in the uow variable and let the caller execute commitWork()

Then the testmethod

@isTest private static void testSuccessPath() {
  fflib_ApexMocks mocks = new fflib_ApexMocks();
  // Given a mock UoW (injected)
  fflib_SobjectUnitOfWork mockUow = 
     (fflib_SobjectUnitOfWork) mocks.mock(fflib_SObjectUnitOfWork.class);
  Application.UnitOfWork.setMock(mockUow);

  // When the email method is invoked
  new MyClass().sendEmail();

  // Then verify that an email was constructed and sent
  ((fflib_SobjectUnitOfWork) mocks.verify(mockUow,
                                          mocks
                                           .times(1)
                                           .description('email sb constructed')))
    .registerEmail((Messaging.SingleEmailMessage) fflib_Match.anyObject());


  ((fflib_SobjectUnitOfWork) mocks.verify(mockUow,
                                          mocks
                                           .times(1)
                                           .description('email sb sent')))
    .commitWork();

  // Then verify that the email was constructed as expected
  // We use ArgumentCaptors for this. There are four (4) steps:

  fflib_ArgumentCaptor capturedEmailArg = 
        fflib_ArgumentCaptor.forClass(Messaging.SingleEmailMessage.class);
  ((fflib_SobjectUnitOfWork) mocks.verify(mockUow,1))
       .registerEmail((Messaging.SingleEmailMessage)capturedEmailArg.capture());

  Object actualEmailAsObject = capturedEmailArg.getValue();
  Messaging.SingleEmailMessage actualEmail = 
        (Messaging.SingleEmailMessage) actualEmailAsObject;

  System.assertEquals('Greetings, earthlings!', 
                       actualEmail.getSubject(),
                       'subject is from friendly aliens');
  System.assertEquals(new list<String> {'[email protected]'},
                       actualEmail.getToAddresses()
                       'only @bar.com domains expected');
  // ... other properties.
}

Benefits

  1. The test will execute even if the sandbox is configured to not send emails
  2. You can assert that the email's properties were set as expected
  3. You can assert that you bothered to build an email and send it for the testmethod's use case (more relevant in a bigger example of orchestrated parts)
  4. You can rely on the unit tests for fflib to actually send the email after registerEmail() + commitWork()

What it doesn't do

  1. If you set properties for the outbound email that are mutually exclusive (like an orgWideEmailAddressId and a senderDisplayName), you won't find out. For this, you will need to actually execute sendEmail (no mocking) when the test runs. You'll need to wrap this in a try-catch as some sandboxes, if not configured to send email, will throw a System.NoAccessException: The organization is not permitted to send email
  2. If your email class uses Messaging.reserveXXXEmailCapcity(..) before sending the email, and your sandbox is configured to not send emails, a System.NoAccessException: The organization is not permitted to send email occurs. You unfortunately as of V41 cannot mock Messaging.reserveXXXEmailCapacity as it is a static method and static methods can't be mocked with the StubAPI.
1
  • Another fantastic answer - decomposition of responsibilities is almost always the answer to testing tricky things. Make them smaller, make them less tricky, then test them. Commented Aug 16, 2018 at 14:19
5

When SFDC is executing testmethods, it will not send any outbound email. This is for a good reason as testmethods would otherwise run the risk of spamming users. Thus, creating an inbound emailservice to capture the email when running a testmethod will not work as the email is never sent.

As @Brian Kessler points out, you can certainly write testmethods to see if you construct the body and other properties of the outbound email by creating a separate class that does the construction, and you can verify that you built everything as expected given a range of possible inputs - this might be useful when you have a complex body/subject to construct.

In my experience, the best way to test the outbound email is to actually run the batch from the sandbox as a user via the Developer Console / Anonymous Apex (perhaps with a constructor argument (..., Boolean forceError) that your code uses to force an error and thus emit the error message via email during finish(). Or, if you are like me most times, merely testing the batch during development will throw errors due to coding mistakes and then you can verify the email.

5

Are you aware of mocking the messaging service? I have an interface with a method sendEmail. In the live code I provide a concrete implementation of the interface which actually sends the email. In the unit test I create a mock implementation of the interface and that implementation can have any logic you want in order to test the various format, if it's sent or not etc. I find this method the most elegant way you could test outbound emails.

For examples if you have automated deployment, your tests will not fail in the targeted sandbox even if the email deliverability is set to No Emails, it doesn't rely on Salesforce.

UPDATE

/*
 * This interface provides all the functions from the Messaging class,
 * and allows us to inject a mock service for unit testing purposes.
 */

public interface IMessagingService {
    Messaging.SendEmailResult[] sendEmail(Messaging.Email[] emails);
}

public class MessagingService implements IMessagingService {
    public Messaging.SendEmailResult[] sendEmail(Messaging.Email[] emails) {
        if (!Test.isRunningTest()) {
            return Messaging.sendEmail(emails);
        } else {
            System.Debug('!!! MessagingService called inside a test. Consider using a mock of IMessagingService.');
            return null;
        }
    }
}

public class AccountRiskOrDefectorNotifier {
    @testVisible private static IMessagingService messagingService;

    static {
         messagingService = new MessagingService();
    }


    //Use messagingService instead of the regular Messaging class
    messagingService.sendEmail(new Messaging.Email[] { mail });
}

public class TestClass(){

    @isTest static void TestMethod(){
        MockMessagingService mockMessagingService = new MockMessagingService();



        System.assertEquals(false, errorOccurred);
        System.assertEquals(true, mockMessagingService.sendEmailCalled);
        System.assertEquals(1, mockMessagingService.sendEmailEmails.size());

        Messaging.SingleEmailMessage email = (Messaging.SingleEmailMessage)mockMessagingService.sendEmailEmails[0];
        //More assertions on the email variable

    }

    private class MockMessagingService implements IMessagingService {

        public Boolean sendEmailCalled = false;
        public Messaging.Email[] sendEmailEmails = null;

        public Messaging.SendEmailResult[] sendEmail(Messaging.Email[] emails) {

            sendEmailCalled = true;
            sendEmailEmails = emails;

            return null;
        }
    }
}
8
  • Fantastic answer - decomposition of responsibilities is almost always the answer to testing tricky things. Make them smaller, make them less tricky, then test them. Commented Aug 16, 2018 at 14:19
  • @Mihai, I'm a bit confused by this code. When testing AccountRiskOrDefectorNotifier, I'm not clear on how the test class/method would utilize MockMessagingService vs. live MessagingService? Commented Oct 25, 2018 at 22:30
  • @mpaler it is determined by the if (!Test.isRunningTest()) line that checks if the messaging service runs in a test context or not. Commented Oct 26, 2018 at 8:41
  • @MihaiNeagoe Thanks for getting back. So in this context, would you implement the Mock at the point where you wrote: System.Debug('!!! MessagingService called inside a test. Consider using a mock of IMessagingService.'); . Like within that conditional bracket? } else { return MockMessagingService.sendEmail(emails); } Something about that doesn't seem right...I guess I'm confused about how the mock class gets implemented vs. the real world class. For example webservice callouts have Test.setMock(). Commented Oct 26, 2018 at 14:00
  • No, you just set the MockMessagingService, in your test, for the @testVisible variable in the Notifier. No need to change any code. You just substitute your implementation of the interface. Commented Oct 26, 2018 at 14:07

You must log in to answer this question.

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