Writing Test Classes for Invocable Methods and Invocable Variables

If you need a primer on invocable methods or invocable variables, check out this blog. To keep things consistent, this post references the same Product Selector pro-bono project for Playworks.

Here is the inner class of invocable variables.

public class Requests{
        
        @invocableVariable(label='Product Id' required=true)
        public String productId;
        
        @invocableVariable(label='Opportunity Id' required=true)
        public String opportunityId;
        
        @invocableVariable(label='Sales Price' required=true)
        public Double salesPrice;
        
        @invocableVariable(label='Quantity' required=true)
        public Integer quantity;
    }

And here is the invocable method, which takes a list of Requests objects as an input parameter.

@InvocableMethod(label='Create New Opportunity Products')
public static void createNewOpportunityProducts(List<Requests> requests){
        
     Requests request = requests[0];
     List<OpportunityLineItem> oppProds = new List<OpportunityLineItem>();
        
     Opportunity oppty = [SELECT Playworks_Fiscal_Date__c 
                          FROM Opportunity                           
                          WHERE Id =: request.opportunityId]; 
        
     PriceBook2 stdPriceBook = [SELECT Id 
                                FROM Pricebook2                                  
                                WHERE isStandard = True 
                                LIMIT 1];       
        
     PriceBookEntry pbe = [SELECT Id 
                           FROM PriceBookEntry 
                           WHERE Product2Id =: request.productId 
                           AND Pricebook2Id =: stdPriceBook.Id
                           LIMIT 1];       
        
     for(Integer i = 0; i < request.quantity ; i++){
         OpportunityLineItem oppProd = new OpportunityLineItem();
         oppProd.Product2Id = request.productId;
         oppProd.PricebookEntryId = pbe.Id;
         oppProd.OpportunityId = request.opportunityId;
         oppProd.UnitPrice = request.salesPrice;
         oppProd.Quantity = 1;
         oppProds.add(oppProd);
     }
     insert oppProds;
}

Note: The way that this Product Selector flow is designed, only one instance of the Requests class is ever populated. Hence index 0 from List<Requests> is assigned.

Requests request = requests[0];

Typically, Apex tests follow the same basic algorithm.

  • Create the data
  • Run the test
  • Validate the assertions

When writing a test for an invocable method with invocable attributes, there are a couple of things to look out for.

Creating the Data

When the createNewOpportunityProducts invocable method is called by the flow, the flow outputs provide the data. Specifically, the flow maps its output variables to what will become attributes of the Requests object at runtime.

The test class is much more manual. Since there is no flow to pass its output variables, we have to create the necessary data.

Pricebook2 standardPricebook = new Pricebook2();
standardPricebook.Id = Test.getStandardPricebookId();
standardPricebook.isActive = true;
update standardPricebook;
        
Product2 prod = new Product2();
prod.Name = 'Test Prod';
prod.IsActive = True;
prod.Product_Selector__c = True;
insert prod;
        
PricebookEntry pbe = new PricebookEntry();    
pbe.Pricebook2Id = standardPricebook.Id;
pbe.Product2Id = prod.Id;
pbe.IsActive = True;
pbe.UnitPrice = 1000;
insert pbe;
        
Opportunity opp = new Opportunity();
opp.Name = 'Test Oppty';
opp.StageName = 'Prospecting';
opp.CloseDate = Date.today();
insert opp;

Once the data is ready to map, we instantiate an object of the Requests class. To do this, we start with the name of the class that contains the invocable method: ProductSelector_Create. That is followed by dot notation to reference the inner class Requests.

List<ProductSelector_Create.Requests> lpscr = new List<ProductSelector_Create.Requests>();

Remember, only one instance of the Requests class is ever populated. So next we instantiate a single ProductSelector_Create.Requests object, map the data that we created earlier to its attributes, and add it to the list of ProductSelector_Create.Requests.

ProductSelector_Create.Requests pscr = new ProductSelector_Create.Requests();
pscr.productId = prod.Id;
pscr.opportunityId = opp.Id;
pscr.salesPrice = 800;
pscr.quantity = 2;
lpscr.add(pscr);

Now we have an input parameter to pass into the createNewOpportunityProducts invocable method.

Running the Test

There is one more thing to be aware of. When an Apex class is called by an Apex trigger, the DML causes the trigger to fire. Since we’re testing an invocable method that is called by a screen flow, no such mechanism exists. Instead of using a DML, we have to explicitly invoke the method, passing in the input parameter we just created.

ProductSelector_Create.createNewOpportunityProducts(lpscr);

Validating the Assertions

Thankfully, there are no real gotchas here. In the Product Selector example, verify that the quantity of 2 caused 2 opportunity products to be created.