In this blog post I’m writing about ApexMocks and one particular feature which I found extremely useful, argument capture. Argument capture can really enhance your unit testing capabilities within FFLIB and ApexMocks based projects.
Before we get started
This blog is heavily based on the application structure found within the FFLIB sample code repository which you find at:
https://github.com/financialforcedev/fflib-apex-common-samplecode/blob/master/fflib-sample-code/
If you’re not familiar with ApexMocks then I highly recommend checking out the Github repository where you can find several links to lots of existing documentation:
https://github.com/financialforcedev/fflib-apex-mocks
The business case
To explain what this feature is I’m going to walk through a simple day to day activity as a developer. We will be creating a Visualforce page controller which is going to create a new account in the system, so long as the customer does not exist in an external system.
We’re going to also create the layers in the correct set up:
- A controller for interacting with the Visualforce page.
- One service layer for creating the customer and asking another layer if a duplicate exists in the external system.
- Lastly, the duplicate account service layer which is responsible for making the callout to the external system.
Our focal point is going to be how to test that our customer service layer in the middle of all of this, correctly calls the duplicate account service layer with the correct information without having to run the actual callout logic.
At this point you might be wondering why not just run the duplicate account service layer logic? Well, we could, but it’s not nice and involves possibly setting up extra test data and even having to mock the callout response in that layer even before we can start testing the layer above it (the customer service layer).
Our controller
Controllers should be as lightweight as possible and shouldn’t include any business logic, ideally. They should capture the input from the user and then offload to a service to enable maximum reuse across the system. This is what our controller is going to do.
public with sharing class CreateCustomerController { public String accountName { get; set; } public PageReference createCustomer(){ CustomerService.newInstance().createCustomer(accountName); return null; } }
As you can see there isn’t much going on here. There is a method which will be called somehow from the page which will call the CustomerService layer and ask for it to create a customer.
Customer service layer
This is our focal point, the layer which is going to create our customer as an account in Salesforce so long as it does not already exist in an external system.
First, we’re going to create an interface to describe how our customer service is going to look like. The one below is very simplistic, it defines one method to allow creating a customer.
public interface ICustomerService { void createCustomer(String name); }
Now we have our interface set up, we can focus on writing the actual business logic.
You will need to be a little bit familiar with the factory pattern to understand what is going on here now, but you can take a look at https://github.com/financialforcedev/fflib-apex-common-samplecode/blob/master/fflib-sample-code/src/classes/Application.cls as a starting point to understand this.
public with sharing class CustomerService implements ICustomerService { public static ICustomerService newInstance(){ return (ICustomerService) Application.service.newInstance(ICustomerService.class); } public void createCustomer(String name){ Account account = new Account( Name = name ); DuplicateAccountService.Request request = new DuplicateAccountService.Request(); request.name = account.Name; Boolean isDuplicate = DuplicateAccountService.newInstance().alreadyExists(request); if(!isDuplicate){ insert account; } } }
Nothing much is going on in terms of the business logic we are running, but do pay attention to how we are calling the duplicate account service. We are creating a request and mapping the provided account name onto the request. It this this part which we will be testing later, did the customer service call the duplicate account service with the correct request.
Now the duplicate account service
Just like before we start out by creating our interface to describe how our service will look like. Again, it’s very simplistic.
public interface IDuplicateAccountService { Boolean alreadyExists(DuplicateAccountService.Request request); }
We’re defining that our service must implement a method which is going to return true or false to indicate whether the account name exists or not.
As we’re not really testing this layer we will just create a skeleton class with no business logic implemented. In a real situation this class would make a HTTP callout and would parse a response.
public with sharing class DuplicateAccountService implements IDuplicateAccountService { public static IDuplicateAccountService newInstance(){ return (IDuplicateAccountService) Application.service.newInstance(IDuplicateAccountService.class); } public Boolean alreadyExists(DuplicateAccountService.Request request){ Boolean isDuplicate = false; // Callout logic goes here... return isDuplicate; } public class Request { public String name; } }
Now we have everything set up for us to begin unit testing it!
Unit testing it!
Lets start with the complete unit test first so you can get a bit of an overview first.
@isTest public static void shouldCallDuplicateAccountServiceWithCorrectRequest(){ // Given fflib_ApexMocks mocks = new fflib_ApexMocks(); IDuplicateAccountService serviceMock = (IDuplicateAccountService) mocks.mock(DuplicateAccountService.class); Application.service.setMock(IDuplicateAccountService.class, serviceMock); // When CustomerService.newInstance().createCustomer('Acme Inc.'); // Then fflib_ArgumentCaptor argument = fflib_ArgumentCaptor.forClass(DuplicateAccountService.Request.class); ((IDuplicateAccountService) mocks.verify(serviceMock)).alreadyExists((DuplicateAccountService.Request) argument.capture()); DuplicateAccountService.Request calloutRequest = (DuplicateAccountService.Request) argument.getValue(); System.assertEquals('Acme Inc.', calloutRequest.name, 'Webservice was not provided the correct request information'); }
To break it down into small chunks:
- We start up ApexMocks.
- Mock the duplicate account service. We’re not going to be running the real logic, so we’re going to mock it.
- Tell the application to use our mock duplicate account service. This is important as if we do not do this the application will not use our mock but will run the real logic instead.
- Run the create customer logic using a test account name.
- Ask the mocked duplicate account service mock to capture the value which was provided into it and verify that the method was called.
- Retrieve the captured value.
- Assert that the request contained our test account name.
Now let’s break it all down in detail and walk through what each part is doing.
Step 1 – Set up ApexMocks
This provides us with all of the goodness.
fflib_ApexMocks mocks = new fflib_ApexMocks();
Step 2 – Mock the duplicate account service
Mock the duplicate account service. We don’t actually want to run the logic within this layer, we only want to know was it called correctly. The mock allows us to isolate the unit test to only the customer service layer but will also allow us to capture the values which were passed into it.
IDuplicateAccountService serviceMock = (IDuplicateAccountService) mocks.mock(DuplicateAccountService.class);
If you’re not familiar with mocking, think of a mock as a class which looks like the real thing but doesn’t contain any business logic within at all. It’s just a mock.
Step 3 – Tell the application to use the mock
It’s great having a mock, but our application isn’t aware of it yet. We need to tell our application to use it. This is done by setting the mock as follows:
Application.service.setMock(IDuplicateAccountService.class, serviceMock);
Step 4 – Run the create customer logic
The mock is set and so we are good to go run our customer service and ask it to create a new customer. Remember, the real duplicate account service isn’t being run now.
CustomerService.newInstance().createCustomer('Acme Inc.');
Step 5 – Capture the argument value and verify that the method was called
Now we’re beginning to utilise the argument capture functionality. We are going to set up an argument capture which will allow us to retrieve the value used when calling the mock. When the mock is called it will record the argument value passed into it and will expose it to our argument captor.
fflib_ArgumentCaptor argument = fflib_ArgumentCaptor.forClass(DuplicateAccountService.Request.class); ((IDuplicateAccountService) mocks.verify(serviceMock)).alreadyExists((DuplicateAccountService.Request) argument.capture());
On the first line we define an argument captor which will allow us to retrieve the value later on. Although not implemented yet within ApexMocks, we need to define the data type which the argument value is an instance of.
The second line we ask ApexMocks to verify that the alreadyExists method within the duplication account service mock was called once, but when verifying capture the value which was passed into it.
Step 6 – Retrieve the captured value
So here is the bit you’ve been finally waiting for! Retrieving the value which was passed into the duplicate account service.
DuplicateAccountService.Request calloutRequest = (DuplicateAccountService.Request) argument.getValue(); System.assertEquals('Acme Inc.', calloutRequest.name, 'Webservice was not provided the correct request information');
The part which makes all of this work is the “argument.getValue()” part. This is asking the recorded argument captured to return the value which was passed into it. As arguments can be of any data type within the system we need to cast it back to the right data type, then we can finally add in our assert methods to check whether the argument did indeed get populated correctly.
Overall
Argument capture is a super useful addition to ApexMocks and really does help transform how we write unit tests within Apex on the Salesforce platform. We can now focus on testing smaller chunks of our applications and test whether they are interacting with other layers correctly, without actually running them!
Look forward to hearing what you think of argument capture!
Leave a Reply