1

Let's say you have this pseudo code for some asynchronous transaction that processes an accountId

Account[] accounts = [SELECT Id, Name FROM Account WHERE Id = :accountId];
if (accounts.isEmpty) {return;} 

// do callout, get back Website

update new Account(Id = accounts[0].Id,Website=valueFromCalloutResponse);

Now, it is possible that some other transaction deletes the account while your callout is in progress (for example, an account merge). The update will throw a DML exception

So, you revise the code to do the following:

Account[] accounts = [SELECT Id, Name FROM Account WHERE Id = :accountId];
if (accounts.isEmpty) {return;} 

// do callout, get back Website

accounts = [SELECT Id, Name FROM Account WHERE Id = :accountId]; // check again
if (accounts.isEmpty) {return;}
update new Account(Id = accounts[0].Id,Website=valueFromCalloutResponse);

How do you set up the testmethod to find the Account on the first SOQL but not find it on the second SOQL?

1 Answer 1

2

You're going to need dependency injection

Step 1 is to replace your SOQL call with a selector class method.

Selector class objects can be mocked and hence their behavior is subject to dependency injection at unit test time. I'll use the fflib pattern by way of example.

Account[] accounts = AccountsSelector.newInstance().selectById(New Set<Id> {accountId});
if (accounts.isEmpty()) {return;} 

// do callout, get back Website

accounts = AccountsSelector.newInstance().selectById(new Set<Id> {accountId}); // check again
if (accounts.isEmpty()) {return;}
update new Account(Id = accounts[0].Id,Website=valueFromCalloutResponse);

Step 2 is to inject stubbed results for the first and second calls to your selector (before and after your callout)

Since we're using fflib by way of example, we'll do dependency injection with apexMocks.

@IsTest
static void givenAccountDeletedWhilstCalloutInProgressVerifyNoUpdate() {

    Account mockAccount = new Account(Name = 'deletedWhilstCallout');
    insert mockAccount;

    // set up your HttpCalloutMock here

    fflib_ApexMocks mocks = new fflib_ApexMocks(); // set up mocking environment

    AccountsSelector mockAccountsSelector = (AccountsSelector) mocks.mock(AccountsSelector.class);
    mocks.startStubbing();
    mocks.when(mockAccountsSelector.sObjectType()).thenReturn(Account.SObjectType);
    mocks.when(mockAccountsSelector.selectById(new Set<Id> {mockAccount.Id}))
            .thenReturnMulti(new List<Object> {
                new List<Account> {mockAccount},   // first SOQL call finds account
                new List<Account> ()               // second call after callout simulates account being deleted
            });
    mocks.stopStubbing();

    // Given mocks injected
    Application.Selector.setMock(mockAssetsSelector);

    // when code-under-test invoked (don't forget to inject your mock HttpCallout here)

    new MyClass().doStuff(someAccountId);

   Account result = [SELECT Id, WebSite FROM Account WHERE Id = :mockAccount.Id][0];
   Assert.isNull(result.Website,'no DML should have occurred as underlying account simulated as deleted');
}

Where's the beef?

The key part is here:

mocks.when(mockAccountsSelector.selectById(new Set<Id> {mockAccount.Id}))
            .thenReturnMulti(new List<Object> {
                new List<Account> {mockAccount},   // first SOQL call finds account
                new List<Account> ()               // second call after callout simulates account being deleted
            });

The thenReturnMulti defines a list of objects. Each object represents the result of the AccountSelector.selectById() method being called successively for the same set of accountIds. Since an object can be a list of Accounts the second call (via dependency injection, returns an empty list - simulating that the Account was deleted mid-transaction by some other thread.

Besides thenReturnMulti, you can also use the fluent approach

mocks.when(mockAccountsSelector.selectById(new Set<Id> {mockAccount.Id}))
            .thenReturn(new List<Account> {mockAccount})   // first SOQL call finds account
            .thenReturn(new List<Account>()) // second call after callout simulates account being deleted
            ;

You can also mix return values and exceptions for successive calls - perhaps to simulate query timeouts

mocks.when(mockAccountsSelector.selectById(new Set<id> {mockAccount.Id}))
            .thenReturn(new List<Account> {mockAccount})   // first SOQL call finds account
            .thenThrow(new System.QueryException(someMsg) // second call simulates QueryException
            ;

Notes

  1. You might think that you can avoid this situation by doing a SELECT ... FOR UPDATE before doing the callout. But no, callouts release all locks.
  2. There are other mocking frameworks out there such as Amoss. Use whichever one you are most comfortable with.

Reference Stub Multiple Calls

You must log in to answer this question.

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