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
- The test will execute even if the sandbox is configured to not send emails
- You can assert that the email's properties were set as expected
- 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)
- You can rely on the unit tests for fflib to actually send the email after registerEmail() + commitWork()
What it doesn't do
- 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
- 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.