DML (Mock) Service

DML (Mock) Service

Fake SaveResult is faster

TL; DR;

Apex utility wrapping DML operations and logging their results consistently. And a mock version of the same allowing to do “pretend” DML in Unit Tests.

Full source on GitHub.

The Problem

Not really a problem to be honest. It started with a desire to reduce duplication in an existing codebase. There was a utility class to do partial DMLs, process SaveResults and log based on errors. When refactoring I decided to go the extra mile and added a Test version of the new service that can help with making our tests much faster (by gradually introducing mocking).

Real DML Service

Partial (allOrNothing: false) or simple DMLs are supported. Nothing special is happening, only “standardised” logging in case of Errors.

public List<Database.SaveResult> databasePartialUpdate(List<SObject> sObjectList, String errorMessageText) {
    String sObjectName = String.valueOf(sObjectList[0].getSObjectType());
    if (String.isBlank(errorMessageText)) {
        errorMessageText = sObjectName + ' DML Update Error';
    }
    List<Database.SaveResult> saveResultList = Database.update(sObjectList, false);
    logErrors(sObjectList, saveResultList, 'UDPATE', sObjectName, errorMessageText);
    return saveResultList;
}

logErrors iterates over the SaveResults and those with isSuccess: false are passed to a method in charge of constructing the full log message and talking to a Logger utility.

The version with allOrNothing: true has to do this slightly differently. This is because in case of error we get a DmlException instead of SaveResults with errors. So the exception is caught and used to build Logger entries. The caller should decide how errors are handled though, so once logged, the DML exception is re-thrown.

public List<Database.SaveResult> databaseInsert(List<SObject> sObjectList, String errorMessageText) {
    String sobjectName = String.valueOf(sObjectList[0].getSObjectType());
    if (String.isBlank(errorMessageText)) {
        errorMessageText = sobjectName + ' DML Insert Error';
    }
    try {
        Database.SaveResult[] saveResultList = Database.insert(sObjectList, true);
        return saveResultList;
    } catch (System.DmlException e) {
        logErrors(sObjectList, e, 'INSERT', sobjectName, errorMessageText);
        throw e;
    }
}

There isn’t any CRUD checking here, I know. But be honest, how many of the DML operations in your org check properly? Small steps..

Full class source is here.

Mocked Version

Finally we get to the interesting part. The Unit Test version of the DmlService which doesn’t actually do the DML. Instead just remembers the SObjects you tried to insert or update and exposes those to test assertions.

Let’s look at the same partial update method:

public static Map<Id, SObject> updatedRecords = new Map<Id, SObject>();

public List<Database.SaveResult> databasePartialUpdate(List<SObject> sObjectList, String errorMessageText) {
    List<Database.SaveResult> saveResultList = new List<Database.SaveResult>();
    for (SObject updatedRecord : sObjectList) {
        Id recordId = updatedRecord.Id;
        if (mockErrorsMessages.containsKey(recordId)) {
            saveResultList.add(
                mockSaveResult(recordId, false, mockErrorsMessages.get(recordId), mockErrorsCodes.get(recordId))
            );
        } else {
            updatedRecords.put(recordId, updatedRecord);
            saveResultList.add(mockSaveResult(recordId));
        }
    }
    return saveResultList;
}

Fake Errors

Testing all scenarios is important. Maybe your method needs to behave differently when some updates fail. The MockDmlService can register an Id (a fake one will do) and some error messages to “throw”.

MockDmlService.registerMockDmlError(
    myRecordId.Id,
    new List<String>{ 'Could not updated Item' },
    new List<String>{ 'CUSTOM_VALIDATION_EXCEPTION' }
);

Registering these fake errors is done using a record id so right now it’s not supported for Insert operations. It’s not that easy to use the hash approach from Flexible Mock. After all it’s the tested code that’s in charge of putting the SObject for insert together. It remains an open point for now, maybe you could help me out?

SaveResults

Returning SaveResult list for the modified or deleted records is an important feature of the DmlService. But it is not possible to instantiate this System type. It is, however, possible to construct it using JSON deserialisation (including the individual error messages).

public static Database.SaveResult mockSaveResult(Id recordId, Boolean success, List<String> messages, List<String> codes) {
    return (Database.SaveResult) JSON.deserialize(
        '{"success":' +
        success +
        ',"id":"' +
        recordId +
        '"' +
        mockErrorsString(messages, codes) +
        '}',
        Database.SaveResult.class
    );
}

private static String mockErrorsString(List<String> messages, List<String> codes) {
    if (messages.isEmpty()) {
        return '';
    }
    String errorString = ',"errors":[';
    for (Integer i = 0; i < messages.size(); i++) {
        errorString += '{"message":"' + messages[i] + '","statusCode":"' + codes[i] + '"},';
    }
    errorString = errorString.removeEnd(',');
    errorString += ']';
    return errorString;
}

In case of a simple update “error” an Exception needs to be thrown. I didn’t have much luck with the JSON approach to build the full object here. But It so happens that it is actually possible to construct a new DmlException. At least type will be right and some message is included. The full Exception structure with field references is not replicated though.

public List<Database.SaveResult> databaseUpdate(List<SObject> sObjectList, String errorMessageText) {
    List<Database.SaveResult> saveResultList = new List<Database.SaveResult>();
    Map<Id, SObject> updatedRecordsMap = new Map<Id, SObject>();
    for (SObject updatedRecord : sObjectList) {
        Id recordId = updatedRecord.Id;
        if (mockErrorsMessages.containsKey(recordId)) {
            DmlException e = new DmlException();
            e.setMessage(String.join(mockErrorsMessages.get(recordId), ','));
            throw e;
        } 
 ...

If you want to see the full class source

What’s Next

I’m planning to update the error mocking to use the FlexibleMock as fake DmlException to throw. That way it could actually return all the details in case calling code depends on that. So far I’ve not needed it though and it needs a bit more testing. I’ll come back to it at some point. And there is still the “errors on insert” thing too. Feel free to take a stab at that and share.