Part two of a series. Posts include:
- Apex Mocks and Enterprise Patterns
- Apex Mocks and Email
- Apex Mocks, Selectors, and Formula Fields
- Apex Mocks and no-arg void Domain methods
- Apex Mocks and Verifying Multiple Custom Type Arguments
One of the problems in unit testing outbound email is that it is hard to verify that you constructed all of the properties of the outbound email as expected. This is compounded by the fact that sandbox orgs default with email deliverability ‘off’ so any attempt to use Messaging.sendEmail(emails)
will throw an exception and your test breaks.
The example’s premise
Suppose we have a simple class method that constructs and sends an email:
public void sendEmail() { Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage(); mail.setToAddresses(new list<String> {'foo@bar.com'}; mail.setSubject('Greetings, earthlings!'); ... Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail }); }
How would you unit test that the outbound email was sent to ‘foo@bar.com’ ? Not so simple. Same for the other properties of the outbound email.
Enter Enterprise Patterns and ApexMocks
Rework the Apex class to use the fflib UnitOfWork:
public void sendEmail() { fflib_ISobjectOfWork uow = Application.UnitOfWork.newInstance(); Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage(); mail.setToAddresses(new list<String> {'foo@bar.com'}; mail.setSubject('Greetings, earthlings!'); ... uow.registerEmail(mail); // let UnitOfWork know mail is part of Txn uow.commitWork(); // send the mail }
The fflib_SobjectUnitOfWorkClass considers outbound emails as part of the transaction, hence the registerEmail(mail)
method call.
Now, your testmethod looks like this:
@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> {'foo@bar.com'}, actualEmail.getToAddresses() 'only @bar.com domains expected'); ... other properties. }
Let’s look at the argumentCaptor, one line at a time.
fflib_ArgumentCaptor capturedEmailArg = fflib_ArgumentCaptor.forClass(Messaging.SingleEmailMessage.class);
We declare a variable of type fflib_ArgumentCaptor
and set it to be of the type we want to inspect.
((fflib_SobjectUnitOfWork) mocks.verify(mockUow,1)) .registerEmail((Messaging.SingleEmailMessage)capturedEmailArg.capture());
We ask the mocking framework that when the UnitOfWork object is called with method registerEmail with single argument of type Messaging.SingleEmailMessage
that we want to capture the value of that argument when the mock UoW is called. The capture() method of ApexMocks library does this. Note we use the mocks.verify(..) method to do this. That is, instead of verifying the value passed to registerEmail we are capturing that value for later inspection.
Object actualEmailAsObject = capturedEmailArg.getValue(); Messaging.SingleEmailMessage actualEmail = (Messaging.SingleEmailMessage) actualEmailAsObject;
Our declared variable capturedEmailArg
has a method getValue()
provided by the fflib_ArgumentCaptor
class. It returns an Object. There is also a getValues()
method for collections. We cast this to the type we care about – Messaging.SingleEmailMessage
.
Now, we can assert against the actual email that the class-under-test constructed and verify each property.
System.assertEquals('Greetings, earthlings!', actualEmail.getSubject(), 'subject is from friendly aliens');
So, why is this cool?
- We are immune from the sandbox having to be configured to send emails. Because fflib is already unit tested, calls to
registerEmail(someEmail)
followed bycommitWork()
will send emails in an org configured to send emails. We merely need to verify thatregisterWork
andcommitWork
got called. Since the UnitOfWork layer is mocked, we can use ApexMocks to verify that calls to a mockUow are as expected. - The
fflib_ArgumentCaptor
feature of ApexMocks allows detailed inspection of arguments passed to any mockable class/method. In our example, it is a single argument of typeMessaging.SingleEmailMessage
, but it could be any arbitrary Apex type. Thus, your unit tests can be quite exhaustive about verifying that the contract between two objects is fulfilled without having to construct any real Sobjects, do any real DML, or query for the results.