Intro to Apex: Auto converting leads in a trigger
by Patrick Connelly posted on July 23, 2014
Over the past couple of weeks I have seen several posts on the developer forums about writing a trigger to auto convert leads based on some criteria. Since this seems to be a pretty common topic, I thought I’d turn it into my first “Intro to Apex” blog post. In this post I am going to introduce a trigger that converts a lead and the test for this trigger. I am going then break down each line of the trigger and explain what it does and why it is there.
NOTE: This is a very basic trigger. If this were to be used in an environment where there was more than just this functionality in the trigger, I would classify this trigger to control the order of operations.
The Requirements
For this example, we have a custom Text field on the Lead object called WebForm that we can see on the Lead Fields page that it’s API Name is WebForm__c that is what we will use while referencing it in the trigger. Our requirements are that when a lead is inserted with the WebForm field equaling “Free Trial” we should auto convert the lead.
The Trigger
trigger AutoConverter on Lead (after insert) {
LeadStatus convertStatus = [
select MasterLabel
from LeadStatus
where IsConverted = true
limit 1
];
List<Database.LeadConvert> leadConverts = new List<Database.LeadConvert>();
for (Lead lead: Trigger.new) {
if (!lead.isConverted && lead.WebForm__c == 'Free Trial') {
Database.LeadConvert lc = new Database.LeadConvert();
String oppName = lead.Name;
lc.setLeadId(lead.Id);
lc.setOpportunityName(oppName);
lc.setConvertedStatus(convertStatus.MasterLabel);
leadConverts.add(lc);
}
}
if (!leadConverts.isEmpty()) {
List<Database.LeadConvertResult> lcr = Database.convertLead(leadConverts);
}
}
Trigger Breakdown
Line 1
Trigger AutoConverter on Lead (after insert) {<
Here we declare our trigger. We give it a name of AutoConverter
and we say that the trigger is on the Lead
object. We then say that it will act only after an insert has occurred.
Line 2-7
LeadStatus convertStatus = [
select MasterLabel
from LeadStatus
where IsConverted = true
limit 1
];
On these lines we make a SOQL call to fetch the LeadStatus
object that coincides with the conversion criteria
Line 8
List<Database.LeadConvert> leadConverts = new List<Database.LeadConvert>();
Since we want to make sure our trigger handles bulk insertion of data, we need to have a place to store all of the leads we need to convert. To do this we declare a variable that is a list of Database.LeadConvert
objects. At this time, it is an empty list.
Line 10
for (Lead lead: Trigger.new) {
This is the start of our for loop. Here we will loop through ever newly created lead in our new list (Trigger.new
), assign it to a temporary variable lead
and start our work. We do this in a for loop to make it so that we can handle bulk inserts.
Line 11
if (!lead.isConverted && lead.WebForm__c == 'Free Trial') {
Here is where we check our business logic and check to see if the lead should be converted. The first part of this if statement says if the lead is not (!) converted and (&&) the WebForm__c
equals (==) Free Trial
then we continue on to lines 12-19. If the lead had already been converted or the WebForm__c
field did not equal Free Trial
our code would have jumped to line 20, and then continued on with the next new lead
Line 12
Database.LeadConvert lc = new Database.LeadConvert();
We create a new Database.LeadConvert
object that we will use to convert later
Line 13
String oppName = lead.Name;
We create a new String variable that holds the lead name that we will be using for the opportunity’s name.
Line 15
lc.setLeadId(lead.Id);
We set the lead id on the lead convert (lc) variable
Line 16
lc.setOpportunityName(oppName);
We set the opportunity name on the lead convert (lc) variable
Line 17
lc.setConvertedStatus(convertStatus.MasterLabel);
We set the converted status of the lead to the MasterLabel
field on our LeadStatus object from lines 2-7 on the lead convert (lc) variable
Line 19
leadConverts.add(lc);
Now that we have a completely filled out Database.LeadConvert
object we add it tor our leadConverts
list
Line 20
}
This is the closing brace for our if statement
Line 21
}
This is the closing brace for our for loop. At this point if there are any more new leads for us to process, we will go back to line 10 and repeat with the next lead
Line 23
if (!leadConverts.isEmpty()) {
Here we check to see if we have actually added any leads for us to convert. If we had skipped lines 12-19 because line 11 was false, we would still have an empty leadConverts
list. We do this because we do not want to waste any DML operations if we do not have to.
Line 24
List<Database.LeadConvertResult> lcr = Database.convertLead(leadConverts);
Here we assign a list of Database.LeadConvertResult
objects (lcr) that are returned from our Database.convertLead call. We pass our list of leadConverts
to the method. This method automatically convert our qualifying leads into new Opportunities with the same name as our lead.
Line 25
}
This is the closing brace of our second if statement
Line 26
}
This is the closing brace of our trigger
The Tests
Now, partially because we have to, and partially because we want to be good developers we need to write tests to cover our trigger. Now we could do somethings that are cheating but doing those just hurt you in the long run. What we want to do is to write tests that mimic how our user is going to interact with the system and verify that our trigger does what we want it to do. For this we will do three tests:
Trial Convert: This test is a positive test of what our trigger should do. We will create a trial lead, insert the data and verify that it changed
Non-trial Convert: This test is a negative test of what our trigger should do. We will create a non-trial lead, insert the data and verify that it did not change
Bulk Test: This is an advanced test that can be kind of tricky to think about. We will create a bunch of leads (half trial, half non-trial) and insert them all at once, then verify that the ones that should have changed did
@IsTest
private class AutoConverter_Test {
private static Integer LEAD_COUNT = 0;
private static Lead createLead() {
LEAD_COUNT += 1;
return new Lead(
FirstName = '_unittest_firstname_: ' + LEAD_COUNT,
LastName = '_unittest_lastname_: ' + LEAD_COUNT,
Company = '_unittest_company_: ' + LEAD_COUNT,
Status = 'Inquiry',
WebForm__c = 'Not Free Trial'
);
}
public static void makeFreeTrial(Lead lead) {
lead.WebForm__c = 'Free Trial';
}
public static List<Lead> fetchLeads(Set<Id> ids) {
return [
select isConverted
from Lead
where Id in :ids
];
}
public static testMethod void trialConvert() {
Lead testLead = createLead();
makeFreeTrial(testLead);
Test.startTest();
insert testLead;
Test.stopTest();
List<Lead> results = fetchLeads(new Set<Id>{testLead.Id});
System.assertEquals(1, results.size(), 'Did not get the right number of leads back');
System.assert(results.get(0).isConverted, 'The lead should have been converted since it was a "Free Trail"');
}
public static testMethod void nonTrialNoConvert() {
Lead testLead = createLead();
Test.startTest();
insert testLead;
Test.stopTest();
List<Lead> results = fetchLeads(new Set<Id>{testLead.Id});
System.assertEquals(1, results.size(), 'Did not get the right number of leads back');
System.assert(!results.get(0).isConverted, 'The lead should not have been converted since it was not a "Free Trail"');
}
public static testMethod void bulkTest() {
List<Lead> shouldBeConverted = new List<Lead>();
List<Lead> shouldNotBeConverted = new List<Lead>();
for (Integer i = 0; i < 50; i++) {
Lead testLeadNonConvert = createLead();
Lead testLeadConvert = createLead();
makeFreeTrial(testLeadConvert);
shouldBeConverted.add(testLeadConvert);
shouldNotBeConverted.add(testLeadNonConvert);
}
List<Lead> toInsert = new List<Lead>();
toInsert.addAll(shouldBeConverted);
toInsert.addAll(shouldNotBeConverted);
Test.startTest();
insert toInsert;
Test.stopTest();
Map<Id, Lead> expectedConversions = new Map<Id, Lead>(shouldBeConverted);
Map<Id, Lead> expectedNonConversions = new Map<Id, Lead>(shouldNotBeConverted);
Set<Id> leadIds = new Set<Id>();
leadIds.addAll(expectedConversions.keySet());
leadIds.addAll(expectedNonConversions.keySet());
for (Lead result: fetchLeads(leadIds)) {
if (expectedConversions.containsKey(result.Id)) {
System.assert(result.isConverted, 'This lead should have been converted ' + result);
expectedConversions.remove(result.Id);
} else if (expectedNonConversions.containsKey(result.Id)) {
System.assert(!result.isConverted, 'This lead should not have been converted ' + result);
expectedNonConversions.remove(result.Id);
} else {
System.assert(false, 'We got a Lead we did not expect to get back ' + result);
}
}
System.assert(expectedConversions.isEmpty(), 'We did not get back all the converted leads we expected');
System.assert(expectedNonConversions.isEmpty(), 'We did not get back all the non converted leads we expected');
}
}
Tests Breakdown
For simplicity sake I will skip the closing braces for these methods, and will skip some redundant lines that appear in multiple tests
Line 1
@IsTest
Here we say that this entire class is a test class
Line 2
private class AutoConverter_Test {
We then declare our test class and call it AutoConverter_Test
Line 3
private static Integer LEAD_COUNT = 0;
This variable is an static Integer, which means it will keep it’s value as it’s changed across a single test run. We will be using it to generate a unique (and trackable) name for our test data. This is important because there are lots of fields (like Name) that must be unique across all records.
Line 5
private static Lead createLead() {
This defines our createLead method. We will use this method in our tests to generate test data and return a new Lead object. I would recommend that you move this type of logic to a separate class (such as TestUtils) so that you can re-use it for all of your tests, not just for a single test class.
Line 6
LEAD_COUNT += 1;
Here we increment our current lead count. The more times we call createLead the higher this number will get.
Line 7-13
return new Lead(
FirstName = '_unittest_firstname_: ' + LEAD_COUNT,
LastName = '_unittest_lastname_: ' + LEAD_COUNT,
Company = '_unittest_company_: ' + LEAD_COUNT,
Status = 'Inquiry',
WebForm__c = 'Not Free Trial'
);
These lines create a test Lead object and return it. Here we create dummy data that has our LEAD_COUNT variable added to it. This could help us if we needed to determine why a test was failing. We could know which Lead (if we had multiples) was causing an issue.
Line 16
public static void makeFreeTrial(Lead lead) {
This defines our makeFreeTrial method. It takes in a Lead object named lead that we will update. This method will update our Lead and make it have the criteria we defined in our business logic that defines a “Free Trial”
Line 17
lead.WebForm__c = 'Free Trial';
We update the WebForm__c field and change it to “Free Trial” CompSci note: Since this passed to us by reference we just need to set the field and it will be updated on the object
Line 20
public static List<Lead> fetchLeads(Set<Id> ids) {
This defines our fetchLeads method. It takes in a Set of Id objects named ids that we will use to find our Leads.
Line 21-25
return [
select isConverted
from Lead
where Id in :ids
];
This returns the results from our SOQL query. This query fetches the isConverted field on the Lead where that Lead’s id is in the set of ids that we were passed in.
Line 28
public static testMethod void trialConvert() {
This is our first test method called trialConvert
Line 29
Lead testLead = createLead();
We create our testLead by calling the createLead method declared on Line 5
Line 30
makeFreeTrial(testLead);
We now take our lead and update it to be a free trial
Line 32
Test.startTest();
This line we tell Salesforce that we are starting our test. At this point we start with all new governor limits. It is very important that you only wrap the parts of your tests that are actually doing the work (ie not setup/verification) in Test.StartTest(); and Test.StopTest(); to properly test that your code will not encounter any governor limits.
Line 34
insert testLead;
We insert our lead
Line 36
Test.stopTest();
We tell Salesforce that we are no longer in our test section
Line 38
List<Lead> results = fetchLeads(new Set<Id>{testLead.Id});
We get a List of leads for our set of ids. In this instance we are only passing in a single Id (generating a new Set inline). We use the method built for bulk so that we do not have to generate code that overlaps in functionality.
Line 40
System.assertEquals(1, results.size(), 'Did not get the right number of leads back');
Our first true “test” in our test class. Here we assert that the number of results we get back from fetchLeads is equal to 1. If we did not get that back then we will display the message “Did not get the right number of leads back.” The order of these parameters is important, because it will affect the way that it is displayed if it fails. The order is Expected, Actual, Message.
Line 41
System.assert(results.get(0).isConverted, 'The lead should have been converted since it was a "Free Trail"');
Since we know we have only one result coming back we verify that the Boolean field isConverted is equal to true on the first (0 index) result from our list. If it is not then we display the error message “The lead should have been converted since it was a “Free Trial”.” Here we use just assert since we are looking at a Boolean value and have nothing to compare it to.
Line 44
public static testMethod void nonTrialNoConvert() {
This is our negative test nonTrialNoConvert
Line 45
Lead testLead = createLead();
We get our new test lead. Unlike before, we will not make it a “Free Trial” to test that it does not auto-convert.
Line 49
insert testLead;
We insert our lead between our Start and StopTest calls
Line 53
List<Lead> results = fetchLeads(new Set<Id>{testLead.Id});
We fetch the leads we just inserted
Line 56
System.assert(!results.get(0).isConverted, 'The lead should not have been converted since it was not a "Free Trail"');
Here we assert that the first record (index 0) is not (!) converted. If it was converted we would see the error message “The lead should not have been converted since it was not a “Free Trial”.”
Line 59
public static testMethod void bulkTest() {
This is our bulkTest. This test is a bit tricky, so don’t be discouraged if it doesn’t make immediate sense. We do this test to assure that if we do large data inserts (think DataLoader) we do not fail.
Line 60
List<Lead> shouldBeConverted = new List<Lead>>();
We generate a new empty list of Leads that will be ones that should be converted
Line 61
List<Lead> shouldNotBeConverted = new List<Lead>();
We generate a new empty list of Leads that will be ones that should not be converted
Line 63
for (Integer i = 0; i < 50; i++) {
Here we loop through 50 integers (0-49) and assign them to the variable i. Once this is complete we will have a total of 100 leads (2 leads x 50 iterations) to insert at one time.
Line 64
Lead testLeadNonConvert = createLead();
We create a new lead called _testLeadNoConvert _this will remain a non free trial lead and should not be converted
Line 65
Lead testLeadConvert = createLead();
We create a new lead called testLeadConvert this will be made a “Free Trial” and should be converted later
Line 66
makeFreeTrial(testLeadConvert);
Make our testLeadConvert lead a “Free Trial” lead
Line 68-69
shouldBeConverted.add(testLeadConvert);
shouldNotBeConverted.add(testLeadNonConvert);
Add the leads to their respective lists
Line 72
List toInsert = new List();
Create a new list of all the leads we are going to insert
Line 73-74
toInsert.addAll(shouldBeConverted);
toInsert.addAll(shouldNotBeConverted);
Add both our shouldBeConverted and shouldNotBeConverted lists together to form one big list of leads toInsert
Line 78
insert toInsert;
Insert all the leads we generated
Line 82-83
Map<Id, Lead> expectedConversions = new Map<Id, Lead>(shouldBeConverted);
Map<Id, Lead> expectedNonConversions = new Map<Id, Lead>(shouldNotBeConverted);
Generate a map of Ids to Leads for both our expectedConversions and our expectedNonConversions. This part is a bit tricky. We need a way to know a couple of things:
- Which lead goes in which bucket
- That the leads we get back are ones we expect
- That we get back all the leads we do expect
- A quick way to generate a set of all the lead ids
By using a map this way, we can do all these things.
Line 85
Set<Id> leadIds = new Set<Id>();
Create a Set of ids that will contain all of the Ids from the Leads we just inserted
Line 86-87
leadIds.addAll(expectedConversions.keySet());
leadIds.addAll(expectedNonConversions.keySet());
Add the keySet of each of the two maps we created on lines 82-83. This will leave us with all the Ids of our newly inserted leads
Line 89
for (Lead result: fetchLeads(leadIds)) {
Here we loop through all the leads that have an Id in our set. We loop over each lead from fetchLeads and store it in the variable result to use inside the loop
Line 90
if (expectedConversions.containsKey(result.Id)) {
We check to see if our expectedConversions map contains the id of result in it’s key list. If it does then we continue on inside the if statement. If not, we jump to line 93
Line 91
System.assert(result.isConverted, 'This lead should have been converted ' + result);
Here we assert that the lead has in-fact been converted. If it was not we would display the error message.
Line 92
expectedConversions.remove(result.Id);
We then remove the lead from the map. We do this so that if for some reason we got it multiple times back we would fail because we should only get it back once.
Line 93
} else if (expectedNonConversions.containsKey(result.Id)) {
If the first if statement returned false then we see if our lead’s id is the expectedNonConversions map. If it is we continue on, if it’s not then we jump to line 96
Line 94
System.assert(!result.isConverted, 'This lead should not have been converted ' + result);
Here we assert that the lead has not been converted. If it was converted we display the error message
Line 95
expectedNonConversions.remove(result.Id);
We then remove the lead from the expected map
Line 96
} else {
If neither of the first if statements evaluated to true then we come here last
Line 97
System.assert(false, 'We got a Lead we did not expect to get back ' + result);
If we’ve gotten to this point then we have gotten back data that we should not have gotten back. So we want to fail the test and alert that we got back data we should not have. Here we do a System.assert(false, ‘…‘) which will always fail the test.
Line 101
System.assert(expectedConversions.isEmpty(), 'We did not get back all the converted leads we expected');
Lastly we check to make sure that we got back all of the _expectedConversions. _If we did, then the map will be empty
Line 102
System.assert(expectedNonConversions.isEmpty(), 'We did not get back all the non converted leads we expected');
Lastly we check to make sure that we got back all of the expectedNonConversions. If we did, then the map will be empty