3

I am attempting to create logic that, on Contact record insert or update, will compare the Phone value of the record to another reference object and conditionally populate a checkbox as a result of the comparison.

The first iteration passed unit and functional testing, but was found to cause Apex CPU timeouts under high volume. Therefore, I made the class queueable.

After converting the class, I am now in a situation where testing in the UI works as desired/expected but each unit test returns this error: System.DmlException: Update failed. First exception on row 0; first error: MISSING_ARGUMENT, Id not specified in an update call: [] here Class.CellPhoneIndentifier.execute: line 91, column 1

There isn't too much out there on same record update in a before trigger insert/update using queueable Apex, but this post seems to indicate this whole approach should not work and that I should be working on Ids instead of SObjects as in a future method.

So I'm confused as to why the UI works, but the tests don't and also why current logic appears to work at all when it apparently shouldn't.

     public class CellPhoneIndentifier implements Queueable  {

    --trimmed--

        public CellPhoneIndentifier (List<Contact> contactList, Map<Id, Contact> oldMap) {
            this.contactList = contactList;
            this.oldMap = oldMap;
        }


        public void execute (QueueableContext context) {    

    --trimmed--

            //Loop through contacts to check and see if they are in the mobile contact list
            for (Contact cont : contactsToCheck) {
                if(mobileContact.contains(cont) && cont.Preferred_Phone_is_Mobile__c == false) {
                    cont.Preferred_Phone_is_Mobile__c = true;
                    contactsToUpdate.add(cont);
                } else if (!mobileContact.contains(cont) && cont.Preferred_Phone_is_Mobile__c == true) {
                    cont.Preferred_Phone_is_Mobile__c = false;
                    contactsToUpdate.add(cont);
                }
            }

            update contactsToUpdate;
        }

--trimmed--

@isTest public class CellPhoneIdentifierTest {

//Get Household recordtype ids
static Id contactHouseholdTypeId = Schema.SObjectType.Contact.getRecordTypeInfosByName().get('Household').getRecordTypeId();

@TestSetup
static void setup() {

    Cellphone_Reference__c wirelessId = new Cellphone_Reference__c(Wireless_Block_Identifier__c = '9206430');
    insert wirelessId;

    List<Contact> testContacts = new List<Contact>();

    Contact nonCellContact = new Contact(LastName = 'nonCellContact',
                                    HomePhone = '920-247-2477',
                                    rC_Bios__Preferred_Phone__c = 'Home',
                                    RecordTypeId = contactHouseholdTypeId);

    testContacts.add(nonCellContact);

    Contact cellContact = new Contact(LastName = 'cellContact',
                            HomePhone = '920-643-0489',
                            rC_Bios__Preferred_Phone__c = 'Home',
                            RecordTypeId = contactHouseholdTypeId);

    testContacts.add(cellContact);

    Contact shortContact = new Contact(LastName = 'shortContact',
                                        HomePhone = '920-6',
                                        rC_Bios__Preferred_Phone__c = 'Home',
                                        RecordTypeId = contactHouseholdTypeId);

    testContacts.add(shortContact);

    Contact nophoneContact = new Contact(LastName = 'nophoneContact',
                                        RecordTypeId = contactHouseholdTypeId);

    testContacts.add(nophoneContact);

    insert testContacts;
}

//Tests a Cell contact being inserted
@isTest
static void insertCell() {
    Contact contactInsert = new Contact(LastName = 'TestContact',
                                    HomePhone = '920-643-0489',
                                    rC_Bios__Preferred_Phone__c = 'Home',
                                    RecordTypeId = contactHouseholdTypeId);

    Test.startTest();

    insert contactInsert;

    Test.stopTest();

    contactInsert = [SELECT Id, Phone, Preferred_Phone_is_Mobile__c FROM Contact WHERE LastName = 'TestContact'];

    System.assertEquals(true,contactInsert.Preferred_Phone_is_Mobile__c);
}
--trimmed--
4
  • 1
    Justin -- (1) too much code here -- leave only the testmethod that fails; use edit and identify the line of failure; (2) in @TestSetup - do you gate these DML to avoid invoking the queueable and then re-enable the queueable upon conclusion of @TestSetup (3) In general, queueable constructor should be passed values that can't change between enqueuing and execution so the first arg should be a set<Id> contactIds. But that won't be your issue here
    – cropredy
    Apr 20, 2019 at 0:35
  • Thank you - (1) I added clarification that every test method is failing and added the line of failure. As all are failing, is still worth removing some methods just so it isn't such a huge block? (2) I am not gating the DML in @TestSetup - what would that entail? (3) This is also something I need to understand more - in what scenarios is it ok to take advantage of Queueable's able to take SObjects?
    – Justin
    Apr 20, 2019 at 0:49
  • 1
    (1) to get the most from the community, you want to show only the minimum necessary to reproduce the issue; see How to Ask. Code dumps not recommended (2) Have some static vbl in the enqueuer class that is @TestVisible that you can turn on/off in testmethod; (3) Your second arg (map<id,sobject> is a good example of why sobject passing is relevant as old value can't change when queueable executes. Or, if you are doing a callout in queueable and the values of the sobject as of the moment of enequeuing should be used in the callout
    – cropredy
    Apr 20, 2019 at 0:54
  • (1) Totally get that though it can be tough knowing what to edit when lacking true understanding of the problem- tried to be more ruthless with the edits this time. (2) got it, makes sense. (3) Very helpful clarification, thank you.
    – Justin
    Apr 20, 2019 at 1:06

1 Answer 1

8

In a before insert trigger, there are no record Ids assigned yet. If you're testing in the UI, try creating a new record, it should have the same error (but it won't be visible in the UI since it is asynchronous). You can move the logic to after insert/after update to avoid this problem.

1
  • 1
    This was very helpful - I ended with after insert/update logic along with a check to avoid recursion. It's clear now why asynchronous triggered actions on the same record belong in the after context.
    – Justin
    May 14, 2019 at 2:44

You must log in to answer this question.

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