5

I am using FINAL to declare a constant on my scheduleable class, which is set to Now, essentially so that I can know what value to test for in my test class. The value of the constant is written to a text field on a record in a daily batch job, if certain conditions are met.

My unit tests are passing. The tests include a test that the constant is not null and also that the value of the text field on the record after the scheduled job is also not null.

But in prod, i am finding that the result after the job is in fact null. The field was populated before, and after the job ran this morning, its been set to null.

So, is the issue that I cant use a constant on a schedulable class? Or that I need to do something involving static? Or that i'm missing something else completely? And why are my tests passing?

global class WebProfileDailyUpdate implements Schedulable {

    public FINAL datetime EXECTIME = datetime.now();

    global void execute(SchedulableContext ctx) {

        //string s = string.valueof(Datetime.now());        
        list<web_profile__c> wp = new list<web_profile__c>();
        for (web_profile__c w : [select Id, Temp_Dummy__c FROM web_profile__c WHERE (Contact__r.LastModifiedDate >= YESTERDAY OR Displayed_Organization__r.LastModifiedDate >= YESTERDAY OR Elected_Organization__r.LastModifiedDate >= YESTERDAY OR Opportunity__r.LastModifiedDate >= YESTERDAY)]) {
            system.debug('wp size = ' + wp.size());
            w.temp_dummy__c = string.valueOf(EXECTIME);
            system.debug('EXECTIME = ' + EXECTIME);
            wp.add(w);
            }            
        IF(wp.size()>=1){
            update wp;
        }
    }
}

TEST CLASS

@istest
public class test_webProfileDailyUpdate {

              public static WebProfileDailyUpdate wpdu = new WebProfileDailyUpdate();  
              public static datetime exec = wpdu.EXECTIME;          

               public static String CRON_EXP = '0 0 0 15 3 ? 2032';

               static testmethod void test() {  

                  system.debug('exec = ' + exec); 
                  datetime dt = datetime.now().addDays(-1);
                  account a = new account();
                  testFactory.createSObject(a);
                  insert a;

                  web_profile__c twp = new web_profile__c(displayed_organization__c = a.id);
                  testFactory.createSObject(twp);
                  insert twp;  

                   Test.startTest();

                  // this should have one record
                  list <web_profile__c> wps = new list<web_profile__c>([select id, temp_dummy__c from web_profile__c where temp_dummy__c = '']);
                  system.assert(wps.size() == 1);

                  // this should have one record
                  list <web_profile__c> wptest = new list<web_profile__c>([select Id, Temp_Dummy__c, Displayed_Organization__c FROM web_profile__c WHERE Displayed_Organization__r.LastModifiedDate >=: dt]);
                  system.assert(wps.size() == 1);

                  // Schedule the test job
                  String jobId = System.schedule('ScheduleWebProfileUpdateClassTest',
                                    CRON_EXP, 
                                    wpdu);

                  // Get the information from the CronTrigger API object
                  CronTrigger ct = [SELECT Id, CronExpression, TimesTriggered, 
                     NextFireTime
                     FROM CronTrigger WHERE id = :jobId];

                  // Verify the expressions are the same
                  System.assertEquals(CRON_EXP, 
                     ct.CronExpression);

                  // Verify the job has not run
                  System.assertEquals(0, ct.TimesTriggered);

                  // Verify the next time the job will run
                  System.assertEquals('2032-03-15 00:00:00', 
                     String.valueOf(ct.NextFireTime));

                  Test.stopTest();

                  list <web_profile__c> wpsu = new list<web_profile__c>([select id, temp_dummy__c from web_profile__c where temp_dummy__c != '']);                            
                  system.assert(wpsu.size() == 1);
                  system.assertEquals(wpsu[0].temp_dummy__c,string.valueOf(exec));
                  system.assertNotEquals(exec, NULL);              

               }

    }
2
  • I suspect the test case isn't picking up the problem as it is all a single transaction. Does the job run successfully outside prod? Apr 15, 2018 at 17:56
  • i had it working fine (in prod and sb) using a local datetime variable in the test class and testing that temp_dummy__c <> null after the job ran, but i wanted a better unit test, and seemed like referencing a constant on the scheduled class was the best way to do so, but then hit the error i posted on sfdc fox answer (static / non static is still a bit murky to me, so mostly trial and error on that front)
    – gorav
    Apr 15, 2018 at 18:36

2 Answers 2

4

There shouldn't be a problem with the test, but your live code has a problem; the value of EXECTIME becomes stored with the class's scheduled state, so it will only ever use the time the class was initially scheduled. Use a static variable instead:

public static final DateTime EXECTIME = datetime.now();

Your code is also relatively inefficient, too. Your scheduled class can be shortened to just:

global class WebProfileDailyUpdate implements Schedulable {
    public static final datetime EXECTIME = datetime.now();
    global void execute(SchedulableContext ctx) {
        list<web_profile__c> wp = [select Id FROM web_profile__c WHERE Contact__r.LastModifiedDate >= YESTERDAY OR Displayed_Organization__r.LastModifiedDate >= YESTERDAY OR Elected_Organization__r.LastModifiedDate >= YESTERDAY OR Opportunity__r.LastModifiedDate >= YESTERDAY];
        String execTimeString = String.valueOf(EXECTIME);
        for (web_profile__c w : wp) {
            w.temp_dummy__c = execTimeString;
        }
        update wp;
    }
}

You don't need to check for empty lists, and you don't need to copy records from one list to another.

Similarly, you've got some unnecessary code in your test:

@istest
public class test_webProfileDailyUpdate {
    public static WebProfileDailyUpdate wpdu = new WebProfileDailyUpdate();  
    public static datetime exec = WebProfileDailyUpdate.EXECTIME;          
    public static String CRON_EXP = '0 0 0 15 3 ? 2032';

    @isTest static void test() {  
        datetime dt = datetime.now().addDays(-1);
        account a = new account();
        testFactory.createSObject(a);
        insert a;

        web_profile__c twp = new web_profile__c(displayed_organization__c = a.id);
        testFactory.createSObject(twp);
        insert twp;  

        Test.startTest();

        // Schedule the test job
        String jobId = System.schedule('ScheduleWebProfileUpdateClassTest',
                                    CRON_EXP, wpdu);
        Test.stopTest();

        list <web_profile__c> wpsu = [select id, temp_dummy__c from web_profile__c];
        system.assertEquals(wpsu[0].temp_dummy__c,string.valueOf(exec));
    }
}

Mostly, the optimizations come down to not testing standard system logic. For example, consider the following unit test:

@isTest static void test() {
  System.assertEquals(4, 2 + 2);
}

What's the point of this unit test? To verify the laws of math haven't changed? Similarly, testing things like making sure the cron job has the same string as it had originally, etc, is all a pointless effort.

Also, minor optimizations here include things like: a query result is a list, so you don't need to construct a new list; queries only need parentheses when mixing AND and OR operators; you don't need to query fields that you're not using the values from; and caching the results of a method call for performance reasons (string.valueof was moved outside the loop).

4
  • the details are appreciated! i tried declaring wpdu.EXECTIME as static, but got the error : "Static field cannot be referenced from a non static context: EXECTIME from the type WebProfileDailyUpdate". all this comes from me trying to build a better test class. I initially used a local variable for exectime, set at test.startTest, and the test was passing (temp_dummy__c == exectime), but that seemed unreliable. i could turn temp_dummy back to datetime and test that it is >= exectime (which i think is what Charles suggests). But i thought, there must be a proper way, thus the constant
    – gorav
    Apr 15, 2018 at 18:25
  • @gorav Yes, the error is because you can only refer to a static variable by static reference, not by an instance. This was a copy-paste error, which I've fixed. I agree that a variable that the test can access is more reliable, but it should always be static, especially if you're testing asynchronous code, because the various methods for kicking off asynchronous code (schedulable, queueable, future, and batchable) all serialize their parameters, so the instance in your test is not the instance that gets executed.
    – sfdcfox
    Apr 15, 2018 at 18:37
  • that makes sense! two notes : the wpdu constructor was removed (WebProfileDailyUpdate wpdu = new WebProfileDailyUpdate();) but wpdu is referenced in line 21 (scheduling the job). i had to put that line back in - i believe that is needed in order to schedule the job. i also had to add @istest as line 1 else i get an error : "Defining type for IsTest methods must be declared as IsTest"
    – gorav
    Apr 15, 2018 at 18:46
  • 1
    @gorav Yes, looks like you've got it, as long as it's similar to what I've written; I went ahead and corrected the answer for future visitors, too.
    – sfdcfox
    Apr 15, 2018 at 19:08
3

Sfdcfox posted while I was typing but I'll finish anyway.

In your unit test you execute the constructor new WebProfileDailyUpdate() which guarantees that it runs the line that declares and fills in your constant. But when the system schedules the class for future runs, it probably constructs the class instance at the time of scheduling and then saves it in some format (sort of like how the state of a Visualforce controller is serialized into a view state).

Every time for the scheduled class to run, the stored copy is translated back into an Apex class instance without running any initializers/constructors. I suspect that because you declared the variable final, it cannot actually store that result back into the class instance but it also never executed the line that retrieved the current Datetime. So it returns null.

Sfdcfox has one correct solution. Setting aside the unit test for a moment, it might also just make more sense to retrieve the Datetime in the first line of the execute() method. In your unit test you could retrieve the current Datetime once before Test.startTest() and once after Test.stopTest() and assert that the time it used is between (or equal to either of) the before/after times.

1
  • thanks! my mucking about is really just me trying to build a better unit test, which asserts that now() from the scheduled class run is the value used on the record. i had it all working w/ local variable in the test and was asserting that temp_dummy__c <> nuil, which historically would be in the top 1% of unit tests i have ever written, but i thought hey, i really should test the actual value of the date used...so maybe the real question is how to properly test a datetime field used in a scheduled class.
    – gorav
    Apr 15, 2018 at 18:30

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .