Salesforce Robbie Duncan Salesforce Robbie Duncan

SObject Datetime Precision and Testing

When does saving and reading an SObject result in a different value? How can we cope with this in tests?

This week I hit one of those little oddities in Apex that I don’t think I’ve seen mentioned before. Most likely this won’t impact you in your day-to-day usage of the platform. I would not have even noticed apart from my failing tests. So what did I encounter?

We like to think that when we save an sObject to the database then read it back we get the same data that was saved. And in general we do. But it appears that with Datetimes this is not necessarily the case. Broadly speaking we get the same value back. Certainly at the level of precision that most people work at. But actually we lose precision when we save.

Before I go further I only noticed because my test was actually writing to the database and reading the record back. This is not best practice. I was only doing this because I had not implemented any form of database wrapper to allow me to mock out the database from my tests.

I was testing a method that updates an SObject setting a Datetime field to the current date and time. My test structure was something like this

@IsTest
private static void runTest() {
// Given
MyObject__c obj = new MyObject__c();
insert obj;
Id objId = obj.Id;
Datetime beforeTest = System.now();
// When
myMethod(objId);
// Then
Datetime afterTest = System.now();
MyObject__c afterObject = [Select DateField__c from MyObject__c where Id=:objId];
System.assert(afterObject.DateField__c >= beforeTest && afterObject.DateField__c <= beforeTest);
}

To my surprise this failed. Doing some debugging the date field was being set and a System.Debug printed out all thee datetimes as being the same. Not too surprising. I would only expect them to vary by a few milliseconds. So using the getTime() method to get the number of milliseconds since the start of the Unix epoch I again debugged.

beforeTest and afterTest were as expected. However the date field was before beforeTest! How is this possible. Well I ran the test a few times before I noticed the pattern. The date field value always ended in 000. So we’d have something like (numbers shortened for brevity):

beforeTest: 198818123
dateField: 198818000
afterTest: 198818456

So saving the DateTime was losing the hundreds of millisecond precision of datetime. In this case storing a value into an SObject and reading it back will result in a different value!

Anyway this is pretty easy to fix in a test by similarly losing the precision from our before and after times as below

@IsTest
private static void runTest() {
// Given
MyObject__c obj = new MyObject__c();
insert obj;
Id objId = obj.Id;
Long beforeTest = (System.now().getTime()/1000)*1000;
// When
myMethod(objId);
// Then
Long afterTest = (System.now().getTime()/1000)*1000;
MyObject__c afterObject = [Select DateField__c from MyObject__c where Id=:objId];
Long testValue = afterObject.DateField__c.getTime();
System.assert(testValue >= beforeTest && testValue <= beforeTest);
}

Read More
Salesforce Robbie Duncan Salesforce Robbie Duncan

Testing Queueable Finalizers Beyond The Limits

What happens when you try to test Finalizers beyond the limits? Well it all goes wrong! Come and find out how

Finalizers on Queueables are fantastic. Finally we can catch the uncatchable. Even if your Queueable hits the governor limits and is terminated your Finalizer will still get run and can do something to handle the exception. A reasonably likely scenario is that the failed Queueable will be re-enqueued with some change to its scope so as it will complete.

An example Queueable is shown below

public with sharing class TestQueueable implements Queueable, Finalizer {
  private Callable function;
  private Integer scope;
  public TestQueueable(
    Callable function,
    Integer scope
  ) {
    this.function = function;
    this.scope = scope;
  }
  public execute(QueueableContext ctx) {
    System.attachFinalizer(this);
    Map<String, Object> params = new Map<String,Object> {
      ‘scope’ => scope
    };
   function.call(‘action’,params);
  }
  public execute(FinalizerContext ctx) {
    if (null != ctx.getException()) {
      System.enqueueJob(
        new TestQueueable(function, scope/2)
      );
    }
  }
}

As you can see this is about the most basic implementation of this pattern possible. As a simple form of dependency injection we use a Callable that will take a scope parameter which will control the amount of work done. If the callable throws we just retry with half the scope.

Like any good developer we now need to test this. A naive test might look something like this

@IsTest
private class TestQueueableTest {
@IsTest
private static void execute_whenNoException_doesNotReenqueue() {
Test.startTest();
System.enqueueJob(
new TestQueueable(new NoThrowCallable(),200);
);
Test.stopTest();
// Verify no re-enqueue
}

private NoThrowCallable implements Callable {
private call(action, args) {
}
}
}

This will test the happy path. In the real world we might want to have a mockable wrapper round System.enqueueJob to make this testing easier (and ot enqueue with no delay and appropriate depth control for initial enqueue). This would make the verification simple (with ApexMocks verify). I’ll consider that out of scope for this post as it does not impact the point.

But what about testing re-enqueue? Well that’s simple right - just throw an exception!

@IsTest
private class TestQueueableTest {
@IsTest
private static void execute_whenNoException_doesNotReenqueue() {
Test.startTest();
System.enqueueJob(
new TestQueueable(new ThrowCallable(),200);
);
Test.stopTest();
// Verifyre-enqueue
}

private ThrowCallable implements Callable {
private call(action, args) {
throw new MockException()
}
}
private MockException extends Exception {}
}

This won’t work. The exception will bubble to the top and be re-thrown at stopTest and the test will fail. Oh no! Ah, well that’s not a problem is it? We can catch exceptions like this

private class TestQueueableTest {
@IsTest
private static void execute_whenNoException_doesNotReenqueue() {
try {
Test.startTest();
System.enqueueJob(
new TestQueueable(new ThrowCallable(),200);
);
Test.stopTest();
} catch (Exception e) {
}
// Verifyre-enqueue
}

private ThrowCallable implements Callable {
private call(action, args) {
throw new MockException()
}
}
private MockException extends Exception {}
}

Now the test will complete and the verification section can run. Note that even without the try/catch in the test the Finalizer would run. We can verify this with debug logs. But what if we only want to re-enqueue on limits exceptions. That makes sense as we are reducing the scope to handle limits. Let’s say we altered our Queueable to look like this

public with sharing class TestQueueable implements Queueable, Finalizer {
  private Callable function;
  private Integer scope;
  public TestQueueable(
    Callable function,
    Integer scope
  ) {
    this.function = function;
    this.scope = scope;
  }
  public execute(QueueableContext ctx) {
    System.attachFinalizer(this);
    Map<String, Object> params = new Map<String,Object> {
      ‘scope’ => scope
    };
    try {
      function.call(‘action’,params);
    } catch (Exception e) {
      // Do something
    }
  }
  public execute(FinalizerContext ctx) {
    if (null != ctx.getException()) {
      System.enqueueJob(
        new TestQueueable(function, scope/2)
      );
    }
  }
}

Now any catchable Exception will be caught and the Finalizer won’t see the exception. But a limits exception cannot be caught and will cause re-enqueue. Great! But our previous test won’t work. We are throwing a catchable Exception. Let’s try again

private class TestQueueableTest {
@IsTest
private static void execute_whenNoException_doesNotReenqueue() {
try {
Test.startTest();
System.enqueueJob(
new TestQueueable(new ThrowCallable(),300);
);
Test.stopTest();
} catch (Exception e) {
}
// Verifyre-enqueue
}

private ThrowCallable implements Callable {
private call(action, args) {
Integer scope = args.get(‘scope’);
for (Integer i = 0;i<scope;i++) {
List<Contact> contacts = [Select id from Contact Limit 1];
}
}
}
}

So now if scope is large enough we’ll get a limits exception for too many SOQL queries. If it’s small enough we won’t. But this test can never succeed. The Limits exception will be re-thrown at Test.stopTest and cannot be caught. So the test will fail.

This means it is impossible to test finalizers with limit exceptions. Unless you know how! If so please get in touch 🙂

Read More