22

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?

10

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!');
        }
    }
}
  • Yeah, that's my problem. I really want to keep the tests and the solutions separate. – Venko Aug 30 '14 at 13:45
  • I really need to teach myself the Stub API. That's actually a really clever solution. – sfdcfox Jun 21 '18 at 6:21
  • @sfdcfox Thanks. The stub API was recently covered in a dev blog post. I'm well aware that I'm not really using it as intended. The System.StubProvider implementation shouldn't be in the target class. This is a snippet from something else I was playing with around custom hierarchy settings in test contexts. – Daniel Ballinger Jun 21 '18 at 8:26
  • Neat trick Daniel. My take on the Stub API is that it's terribly ugly (and limited in things like not being able to mock system classes). I'd rather use a Factory to provide the bit that I'm planning to mock, then swap in the mock version via @TestVisible for the test. It means you don't have to have a massive if..else looking at the stubbedMethodName inside your mock. Instead, you can do it with proper interfaces and clean code. – Aidan Jun 21 '18 at 18:30
  • @DanielBallinger Thank you for updating with a solution to simulate the UNABLE_TO_LOCK_ROW error. I've marked your answer as the solution. – Venko Jan 23 at 17:38
2

Test methods do not actually run asynchronous operations asynchronously. All async requests (future calls, queueable Apex, batches, etc.) get queued up and are run synchronously when Test.stopTest is run.

So you will never be able to intentionally create a DML lock in a unit test (though when unit tests are run in parallel you may see them, particularly with custom settings, which should never actually be created or updated in a unit test - which is another story).

You'll want to test this the way you would any other exception handler, by throwing an error, or moving your handling code into a separate method and calling it directly.

Don't stress about keeping tests and solutions separate - that's a preference, not a religious dictate, and this is exactly the kind of scenario that Test.isRunningTest() exists to handle.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.