2

We have an object (let's call it Child) with a formula field that does a lot of checking of various fields and spits out a numeric result (estimated number of days some work will take, based on a bunch of flags and conditions).

Another object (Parent) has multiple Children related to it through a lookup.

I wrote an apex class that aggregates the Children of a Parent in various ways based on several fields, including the formula result. I now want to test that logic, but when I try to create some sample Children to feed into my test, I'm told that I can't write to formula fields.

I don't want to figure out how to build a tree of objects and set specific fields to get the value I want to test from the formula field. Furthermore, the formula has a limited set of results, which may change in the future (say, the business decides that some particular type of work takes 8 days instead of 10). I want to test various edge cases that the formula can't produce today.

What's the best approach to this?

4

Since recalculateFormulas() still has a bug, you can use this as a work around to set formula fields:

@TestVisible
private static SObject setFormulafield(SObject sObj, String fieldName, Object value)
{
    String jsonString = JSON.serialize(sObj);
    Map<String,Object> dataMap = (Map<String,Object>)JSON.deserializeUntyped(jsonString);
    dataMap.put(fieldName, value);
    jsonString = JSON.serialize(dataMap);
    return (SObject)JSON.deserialize(jsonString, SObject.class);
}

Also a nice thing to use when Mocking.

3

The general solution to this problem is two-fold

  1. If the formula logic is especially complex and you want to maintain a regression test suite for it, build an apex testmethod just for it, passing in various combinations of inputs. How you create that test input can be explicitly via APEX SObject construction or perhaps with a library of JSON data kept in a StaticResource
  2. Use a @TestVisible property in your code that returns the value of the formula field such as this:

    @TestVisible
    public Decimal theFormulaFieldVal {
     {get
       return theFormulaFieldVal == null 
         ? theSobject.realFormulaField 
         : theFormulaFieldVal;
     }
     set;
    }
    

Your code never refers to the sobject.formulaField, instead replace all such references to this special property

Then, in your testmethod, after constructing the class,

 theClass.theFormulaField = 10; // mocked value
 // now invoke the class's methods

This way, the formula field returns a constant 10 regardless of use case in testmethods and returns the actual formula value in PROD use cases.

Should the above be too limiting, you can write a more sophisticated mockable facade using your own class with injection via constructor arguments - similar to how HttpMock works.

2

As for setting the value, you really only control the inputs and do, as you say, have to set specific fields to get the value I want. If you want the field to be writeable, move the formula to a Workflow Field Update, and add something in your Workflow Rule criteria to prevent overwrites.

If you want to be able to get the value without the overhead of an insert/query, try the SObject method recalculateFormulas() method. From the documentation:

recalculateFormulas()
Recalculates all formula fields on an sObject, and sets updated field values. Rather than inserting or updating objects each time you want to test changes to your formula logic, call this method and inspect your new field values. Then make further logic changes as needed.

Signature
public Void recalculateFormulas()

Return Value
Type: Void

Usage
This method doesn’t recalculate cross-object formulas. If you call this method on objects that have both cross-object and non-cross-object formula fields, only the non-cross-object formula fields are recalculated.

See Also:
What Are Cross-Object Formulas?

  • I'm not sure I understand. Do you mean I should blow away the formula logic during the test, replace it with a static number, call recalculateFormulas(), and then undo the changes? Is this possible? – Egor Sep 1 '16 at 22:37
  • The formula will still calculate the same as if it were in the database. You have to set up the input data so it yields the values you want. I don't think there's any way around that. This just helps you if you don't want to insert and re-query the data. – Adrian Larson Sep 1 '16 at 22:38
  • Thanks, but my problem is specifically that setting up the input data is lots of work at best, and impossible at worst. The formula has a limited set of outputs, so there's no input combination that will produce some of the outputs I want to test. – Egor Sep 1 '16 at 22:41
  • @Egor Your only other option that I know of is to move the logic to PB/WFR. Or move it to a trigger and then you have lots of control...but the calculation may be harder to configure. – Adrian Larson Sep 1 '16 at 22:43
  • 1
    @Egor - One of the reasons to consider when you find something difficult to test or not future proof is that your implementation may not be ideal. – Eric Sep 1 '16 at 23:00

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.