Is it possible to simulate record locks in test methods?

I would like my test cases to also test exception handling. In theory an easy way to cause an exception on DML is to lock a record that it’ll update so that the update will fail. But I’m having trouble implementing this in a test method.

I’ve tried something along the following lines (may be a little rough, I just typed it straight into SSE):

User locker = TestUtils.createUser();
User deleter = TestUtils.createUser();
Account a = TestUtils.createAccount();
insert a;
System.runAs(locker) {
  Account lockme = [SELECT Id FROM Account WHERE Id=:a.Id FOR UPDATE];
}
System.runAs(deleter) {
  Account delme = [SELECT Id FROM Account WHERE Id=:a.Id];
  SomeClassInstance.MethodToTestThatDelsAccount(delme.Id);
}

I believe that once the runAs(locker) block finishes the lock on the account is released. I’d very much like to ensure that the record is locked when the other user attempts to delete it. Has anyone solved this problem before or is it impossible with tests?

Answer

I don’t believe runas(User) is going to isolate the locking within the transaction. The sObject record locking spans the entire transaction, regardless of the runas statements.

You will probably need to simulate the concurrency UNABLE_TO_LOCK_ROW DmlException using Test.isRunningTest() and another type of exception. You could either cause an alternative type of DmlException or throw something specific to the test case. Neither is ideal as you are bringing elements of the test into the actual code.


Update:

It is now possible to directly throw a DMLException of your own making.

DmlException e = new DmlException();
e.setMessage('UNABLE_TO_LOCK_ROW');
throw e;

It is also possible to use the Stub API to simulate the return result of a public method.

In theory you could combine these two abilities to force what looks like an UNABLE_TO_LOCK_ROW DMLException in a test context without having to modify the actual code. You will need to ensure that the method to be stubbed is public and not static.

Proof of concept

Those with a sensitive testing disposition may want to look away. This is by no means the correct way to do dependency injection and inversion of control for stub testing. Among many other things, the stub for the class shouldn’t be an inner class of the one being stubbed.

At the least it does demonstrate throwing a DMLException that looks like a locked record.

Class to be mocked:

public class DoesTheDml {
    // Must be public so it can be mocked!
    public void updateAccount(Id accountId) {
        Account accountToUpdate = [Select Id, Description from Account where Id = :accountId FOR UPDATE];
        accountToUpdate.Description = 'Updated';
        update accountToUpdate;
        System.assert(false, 'Bang! Actually interested in the mock firing');
    }

    public static DoesTheDml Instance {
        get {
            if(Instance == null) {
                Instance = new DoesTheDml();
            }
            return Instance;
        }
        private set {
            Instance = value;
        }
    }

    public static void updateTheAccount(Id accountId) {
        Instance.updateAccount(accountId);
    }

    private static DoesTheDml mockSettings;
    @TestVisible
    private static DoesTheDml createMock() {
        // Use static instance to avoid creating a new mock with each call
        if(mockSettings == null) {
            mockSettings = (DoesTheDml)Test.createStub(DoesTheDml.class, new DoesTheDmlMockProvider());
            System.debug('DoesTheDml.createMock() completed - new ' + mockSettings);
        } else {
            System.debug('DoesTheDml.createMock() completed - existing');
        }
        Instance = mockSettings;
        System.assertEquals(mockSettings, Instance);

        return mockSettings;
    }

    public class DoesTheDmlMockProvider implements System.StubProvider {
        public Object handleMethodCall(Object stubbedObject, String stubbedMethodName, 
            Type returnType, List<Type> listOfParamTypes, List<String> listOfParamNames, 
            List<Object> listOfArgs) {

            if(stubbedMethodName == 'updateAccount') {

                // Simulate the record locking result if a record is locked for update
                DmlException e = new DmlException();
                e.setMessage('UNABLE_TO_LOCK_ROW');
                throw e;

                //return null;
            } 

            System.assert(false, 'unmocked stubbedMethodName: ' + stubbedMethodName);

            return null;
        }
    }
}

Test class:

@IsTest
public class DoesTheDml_Test {
    @IsTest
    public static void updateTheAccountTest() {
        // Configure the Stub/Mock response
        DoesTheDml instance = DoesTheDml.createMock();

        Id mockAccountId = '001000000000001';

        try {
            DoesTheDml.updateTheAccount(mockAccountId);
            System.assert(false, 'Expected Exception');
        } catch (DmlException dmlEx) {
            System.assert(dmlEx.getMessage().contains('UNABLE_TO_LOCK_ROW'),
                'Expected UNABLE_TO_LOCK_ROW race condition');

            System.debug('We did it!');
        }
    }
}

Attribution
Source : Link , Question Author : Venko , Answer Author : Daniel Ballinger

Leave a Comment