0

I am trying to utilize this Apex Class supplied by Salesforce but when i try to deploy to production i get a code coverage of 68%:

global class ConvertLeadAction {
    @InvocableMethod(label='Convert Leads')
    global static List<ConvertLeadActionResult> convertLeads(List<ConvertLeadActionRequest> 
    requests) {
        List<ConvertLeadActionResult> results = new List<ConvertLeadActionResult>();
        for (ConvertLeadActionRequest request : requests) {
            results.add(convertLead(request));
        }
        return results;
    }

    public static ConvertLeadActionResult convertLead(ConvertLeadActionRequest request) {
        Database.LeadConvert lc = new Database.LeadConvert();
        lc.setLeadId(request.leadId);
        lc.setConvertedStatus(request.convertedStatus);

        if (request.accountId != null) {
            lc.setAccountId(request.accountId);
        }

        if (request.contactId != null) {
            lc.setContactId(request.contactId);
        }

        if (request.overWriteLeadSource != null && request.overWriteLeadSource) {
            lc.setOverwriteLeadSource(request.overWriteLeadSource);
        }

        if (request.createOpportunity != null && !request.createOpportunity) {
            lc.setDoNotCreateOpportunity(!request.createOpportunity);
        }

        if (request.opportunityName != null) {
            lc.setOpportunityName(request.opportunityName);
        }

        if (request.ownerId != null) {
            lc.setOwnerId(request.ownerId);
        }

        if (request.sendEmailToOwner != null && request.sendEmailToOwner) {
            lc.setSendNotificationEmail(request.sendEmailToOwner);
        }

        Database.LeadConvertResult lcr = Database.convertLead(lc, true);
        if (lcr.isSuccess()) {
            ConvertLeadActionResult result = new ConvertLeadActionResult();
            result.accountId = lcr.getAccountId();
            result.contactId = lcr.getContactId();
            result.opportunityId = lcr.getOpportunityId();
            return result;
        } else {
            throw new ConvertLeadActionException(lcr.getErrors()[0].getMessage());
        }
    }

    global class ConvertLeadActionRequest {
        @InvocableVariable(required=true)
        global ID leadId;

        @InvocableVariable(required=true)
        global String convertedStatus;

        @InvocableVariable
        global ID accountId;

        @InvocableVariable
        global ID contactId;

        @InvocableVariable
        global Boolean overWriteLeadSource;

        @InvocableVariable
        global Boolean createOpportunity;

        @InvocableVariable
        global String opportunityName;

        @InvocableVariable
        global ID ownerId;

        @InvocableVariable
        global Boolean sendEmailToOwner;
    }

    global class ConvertLeadActionResult {
        @InvocableVariable
        global ID accountId;

        @InvocableVariable
        global ID contactId;

        @InvocableVariable
        global ID opportunityId;
    }

    class ConvertLeadActionException extends Exception {}
}

I tried to create a test class to just simulate the Lead conversion and get coverage above 75%, but am having issues calling the method Salesforce supplied.

@isTest
private class ConvertLeadActionTest {
      
    static testMethod void testLeadConv() {
        Test.StartTest();  
        Lead myLead = new Lead(LastName = 'Fry', Company='Fry And Sons', Industry = 'Multifamily Residential');
        insert myLead;

        Database.LeadConvert lc = new Database.LeadConvert();
        lc.setLeadId(myLead.id);

        LeadStatus convertStatus = [SELECT Id, MasterLabel FROM LeadStatus WHERE IsConverted=true LIMIT 1];
        lc.setConvertedStatus('SQL');

        Database.LeadConvertResult lcr = Database.convertLead(lc);
        System.assert(lcr.isSuccess());
        Test.StopTest();  
    }
}

I am not sure if this is even necessary. I am a bit new to Apex so any insight would be greatly appreciated and apologies if I am not formatting this question correctly.

2
  • Can you go into a bit more detail about what you're trying to accomplish? Do you actually want to deploy this class (which is less about converting leads and more about making this available for use in a Flow or as a custom lightning action)? Are you trying to learn about unit testing? Or is it something else?
    – Derek F
    Commented Jul 24, 2023 at 19:46
  • I want to deploy this class so that it may be used in a flow that i have created. I am using this class so that I can set specific values on the Converted Lead. I have created the class in my sandbox and have added it to my flow so that i can pass other variables from my flow into the apex class. Everything works as i want in my sandbox, now i am trying to deploy flow, apex class, and everything else to production but am not meeting the 75% code coverage. Commented Jul 24, 2023 at 19:51

2 Answers 2

1

You're not actually unit testing your class. It is being tested from another unit test elsewhere. The test you provided would have 0% direct coverage of this code. A valid unit test for this class would look like:

@isTest static void testConvertLeadAction() {
  Lead leadRecord = new Lead(LastName='Test',Company='Test');
  insert leadRecord;
  Account accountRecord = new Account(Name='Test');
  insert accountRecord;
  Contact contactRecord = new Contact(LastName='Test', AccountId=accountRecord.Id);
  insert contactRecord;
  LeadStatus convertStatus = [SELECT Id, MasterLabel FROM LeadStatus WHERE IsConverted=true LIMIT 1];
  ConvertLeadAction.ConvertLeadActionRequest request = new ConvertLeadAction.ConvertLeadActionRequest();
  request.leadId = leadRecord.Id;
  request.convertedStatus = convertStatus.MasterLabel;
  request.accountId = accountRecord.Id;
  request.contactId = contactRecord.Id;
  request.overwriteLeadSource = true;
  request.createOpportunity = true;
  request.opportunityName = 'Test';
  request.OwnerId = UserInfo.getUserId();
  request.sendEmailToOwner = true;
  Test.startTest();
  ConvertLeadAction.ConvertLeadActionResult[] result = ConvertLeadAction.convertLeads(
    new ConvertLeadAction.ConvertLeadActionRequest[] {
      request
    }
  );
  Test.stopTest();
}

This still won't hit 100% coverage, but it should get something like 95%.

5
  • Hello and thanks for your response! When i put the code into my test class and save i get an error: Method does not exist or incorrect signature: void convertLead(List<ConvertLeadAction.ConvertLeadActionRequest>) from the type ConvertLeadAction Commented Jul 24, 2023 at 20:28
  • Change it to convertLeads
    – fred
    Commented Jul 24, 2023 at 20:32
  • @fred like this? ConvertLeadAction.ConvertLeadActionResult result = ConvertLeadAction.convertLeads( new ConvertLeadAction.ConvertLeadActionRequest[] {request} ); When i add this, this is the new error message Illegal assignment from List<ConvertLeadAction.ConvertLeadActionResult> to ConvertLeadAction.ConvertLeadActionResult Commented Jul 24, 2023 at 20:36
  • 1
    @ZackGibson Yeah, there were a couple of typos. It should have been convertLeads and the return type should have been List<ConvertLeadAction.ConvertLeadActionResult> (I use the [] notation; see edited answer).
    – sfdcfox
    Commented Jul 24, 2023 at 20:46
  • That worked! Thanks a ton Commented Jul 24, 2023 at 20:59
1

Overview

The general goal in unit testing is to stress the class that you're trying to deploy (the System Under Test), and that comes down to 3 main tasks "AAA"

  • Arrange (set up the data that your SUT needs to be able to run)
  • Act (execute a method, usually just a single method per test, of your SUT)
  • Assert (gather results, and compare against the data that you think you should get by running your SUT)

Creating and inserting a test Lead record is part of the "Arrange" step

Executing a target method is what gets you coverage (you only get coverage for lines of code that get executed as part of a test)

And the assertions are generally what make tests useful (because you can get coverage for and deploy a method that says 2 + 2 = 5, but an assertion will help alert you to that incorrect result).

Understanding testing

If I had to guess, the coverage you're getting is probably for the invocable variables of ConvertLeadActionRequest and ConvertLeadActionResult, because nowhere in your current test are you actually calling ConvertLeadAction.convertLeads()

Since convertLeads() is a static method, we call it using <class name>.<method name> (as opposed to an instance method, where you need to create an instance of the class to be able to call the method). So, ConvertLeadAction.convertLeads();

You also need to pass the expected number and type of arguments into the method, which means your test needs to create at least one ConvertLeadActionRequest instance.

So your test method starts to look like this

// The "testMethod" keyword is deprecated, and should no longer be used
// Instead, use the "@isTest" annotation on each of your test methods (and also
//   for the overall test class)
@isTest
static void testLeadConv() {
    // Step 1: Arrange
    // Create the data your code needs to be able to run
    Lead myLead = new Lead(LastName = 'Fry', Company='Fry And Sons', Industry = 'Multifamily Residential');
    insert myLead;

    // In general, we need to create all of our test data ourselves because tests
    //   don't have access to "actual" data by default (test data isolation, this is
    //   a good thing).
    // LeadStatus is an exception to that
    LeadStatus convertStatus = [SELECT Id, MasterLabel FROM LeadStatus WHERE IsConverted = true LIMIT 1];
    // ConvertLeadActionRequest is an "inner class" (a class defined inside of another
    //   class).
    // Since this code is not in the "outer class", we need to use
    //   <outer class name>.<inner class name> to reference/use the inner class
    ConvertLeadAction.ConvertLeadActionRequest request = new ConvertLeadAction.ConvertLeadActionRequest();

    // Use dot-notation to set data for instance variables
    request.leadId = leadRecord.Id;
    request.convertedStatus = convertStatus.MasterLabel;
    request.overwriteLeadSource = true;
    request.createOpportunity = true;
    request.opportunityName = 'Test';
    request.OwnerId = UserInfo.getUserId();
    request.sendEmailToOwner = true;

    // Now that we're done with setup, we can call Test.startTest();
    // We want to wait until _after_ setup to call it because it gives you a fresh
    //   set of governor limits, and does some other things.
    // It can only be called once in a given test method
    Test.startTest();

    // Nowhere in this test method do you want to call Database.LeadConvert
    // That's work that your SUT is supposed to do
    // Instead, call your SUT
    
    // Since convertLeads() expects a List of ConvertLeadActionRequest
    //   you'll need to create a List of ConvertLeadActionRequest
    // Even if the list only holds a single record, (or no records at all) it's 
    //   still a list
    List<ConvertLeadAction.ConvertLeadActionResult> results = ConvertLeadAction.convertLeads(
        // Using new List<thing>() calls the list constructor
        // Using new List<thing>{} instead allows us to initialize the list with some variables
        //   (comma separated if there are more than one)
        new List<ConvertLeadAction.ConvertLeadActionRequest>{ request }
    );

    // stopTest() causes asynchronous processes to be run, so that you can actually
    //   have them affect test data
    Test.stopTest();  
}

This test is not tested, so don't be surprised if it fails for one reason or another. I'm also not making any assertions here, but you really, really should have assertions. You'll generally assert on the results of running your code. In this case, that would be something like

  • Query for Opportunities before calling Test.startTest();
  • Query for Opportunities again, after calling Test.stopTest();
  • Assert that you have one more Opportunity after than before (i.e. your Lead became an Opportunity)

Closing notes

Another general rule of unit testing is that you only want to test one thing at a time. It's very common (and generally good practice) to have multiple test methods, each testing a slightly different scenario (what happens if this piece of data is null? what happens if that thing that's supposed to be greater than 0 is negative? etc...). It's not so important in this particular test class, but if you ever have a chain of if-else if-else then that's a sign that you'll want multiple test methods.

Breaking tests up like that makes it easier to figure out which part of your code (if any) is broken. Also, if you test enough scenarios (both "positive cases" where the data conforms to your expectations, and "negative cases" where there is some issue with the data), you'll naturally have high coverage.

In general, unit testing one thing ends up being very similar to unit testing pretty much anything else. Once you have a decent grasp on it, testing method X in class Y is just a matter of "make sure you directly call method X of class Y".

1
  • Thanks you very much for taking the time to write this explanation out. Very very informative. Commented Jul 24, 2023 at 20:59

You must log in to answer this question.

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