3

I have a batch class that is used to clear out all the records of an object. I'm trying to unit test the case where one of the records fails to be deleted. I'm trying to have a record fail by locking it in my unit test.

public static testMethod void testDeleteFailure() {
  //Test data is setup and I have various assertions here to make sure my test data is good

  Test.StartTest();
  List<My_Object__c> lockedItems = [SELECT Id FROM My_Object__c LIMIT 2 FOR UPDATE];
  BatchDeleteObject deleteJob = new BatchDeleteObject('My_Object__c');
  Database.executeBatch(deleteJob);
  Test.StopTest();

  Integer newCount = [SELECT COUNT() FROM My_Object__c];
  System.assertEquals(lockedItems.count(), newCount, 'There should be ' + lockedItems.count() + ' still in the database.');
}

However, my records aren't locking so they are being deleted just fine. Is there another way to lock these records? Is there a better way to test to make sure any failures are handled appropriately?

  • 1
    It's for the entirety of the transaction. If that strategy were to work you would have to call the query before Test.startTest. – Adrian Larson Feb 19 '18 at 14:12
  • I tried it in both places, just in case, this just happened to be the configuration when I asked the question. – Curious Blueprints Feb 19 '18 at 14:13
  • The documentation is kind of cagey about where the actual transaction boundaries are with Test.{start, stop}Test() (it's described as resetting limits only), particularly when async jobs are submitted. Does the batch class execute in the same transaction as the FOR UPDATE? – David Reed Feb 19 '18 at 14:45
1

I recommend you break your logic into smaller, more testable chunks so you can more easily test just your handling of DmlException. Then you can apply a technique like deleting the records before passing them into the method.

Your batch would look something like the following plus/minus the rest of your logic:

public void execute(BatchableContext context, List<SObject> records)
{
    MyObjectService.safeDelete(records);
}

Then your test would do something like:

@IsTest void testSafeDelete() { /*happy path*/ }
@IsTest
static void testSafeDelete_DmlException()
{
    List<MyObject__c> records = new List<MyObject__c>();
    // add some data
    insert records;
    delete records;

    Test.startTest();
        MyObjectService.safeDelete(records);
    Test.stopTest();

    // assert on behavior
}
  • That's a good idea. I have not seen the deletion of records before. Unfortunately, I didn't write the batch class, so I don't know how tricky this will be. But this should work, thanks. – Curious Blueprints Feb 19 '18 at 14:25
  • This did work for me. All I ended up doing was breaking out the execute method to a separate method and testing that directly. Thanks. – Curious Blueprints Feb 19 '18 at 14:38
  • @CuriousBlueprints I treat batch methods just like triggers. Don't put the logic in the body. – Adrian Larson Feb 19 '18 at 15:17
0

Posting separately since it's so wildly different. This approach forces you to make your batch stateful, so I would avoid it unless your batch is already marked as such.


Another option would be to invoke some sort of proxy.

public virtual class DmlProxy
{
    public virtual void insertRecords(List<SObject> records) { insert records; }
    public virtual void updateRecords(List<SObject> records) { update records; }
    public virtual void deleteRecords(List<SObject> records) { delete records; }
    public virtual void undeleteRecords(List<SObject> records) { undelete records; }
}

Have your batch class store the type and construct it dynamically.

public class MyBatch
{
    @TestVisible Type proxyType = DmlProxy.class;
    //...
    public void execute(/*params*/)
    {
        DmlProxy proxy = (DmlProxy)proxyType.newInstance();
        proxy.deleteRecords(scope);
    }
    //...
}

Then in your test class you can define a separate proxy to use, and inject that dependency.

class FailDelete extends DmlProxy
{
    public override void deleteRecords(List<SObject> records)
    {
        throw new DmlException();
    }
}
@IsTest static void testMyBatch_DmlException()
{
    //data setup

    Test.startTest();
        MyBatch batch = new MyBatch();
        batch.proxyType = FailDelete.class;
        Database.executeBatch(batch);
    Test.stopTest();

    // assertions
}
  • Yes, I could see how this would work, but for an existing batch class (that is fairly simple) I think this is probably overkill for my scenario. – Curious Blueprints Feb 19 '18 at 14:39
  • Indeed. Just an alternative strategy you should be aware of if you run into similar situations in the future. – Adrian Larson Feb 19 '18 at 15:17

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.