Automating Salesforce CPQ Testing

With three automatic upgrades released each year, finding time to test the latest Salesforce CPQ features manually can be tedious and time-consuming due to the product’s complexities. Automated tests are the best way to achieve predictable and repeatable assessments of the quality of your custom code and configuration. With these testing tips, you can begin your automation strategy for Salesforce CPQ customizations at the right layer.

You may be asking “What does CPQ mean?” CPQ stands for Configure, Price, Quote and is a native add-on for Sales Cloud or Service Cloud orgs. If you’re new to CPQ, you can learn more here.

Automated testing allows you to quickly get feedback about the quality and health of your custom code and configuration. In this article, we explore four kinds of automated tests that are useful when testing your CPQ flows.

Let’s start with some general guidelines for writing automated tests:

  1. Test at the right layer. Full Integration UI tests are easy to conceptualize, but hard to run and maintain. Write tests at the right layer. If you want to test only calculation, consider writing a test using the CPQ Calculate Quote API. If you’re writing a custom UI component, write component tests.
  2. Keep it to very few assertions per test. A test should almost always evaluate only one thing, so ideally you can have only one or at most a few assertions. However, when writing tests that are expensive to setup or run, it’s best to use a threshold to break tests down. We recommend three assertions as a good threshold to use. This is particularly important when dealing with a Black Box as it’s easy to get carried away and write very complex scenarios.
  3. Keep tests mutually exclusive and idempotent. Every test should set up the application to its needs and clean up when done. A test should also produce the same result no matter how you run it — individually, as a small suite, as a large set of suites etc. You can have some variations if setup is very complex or cost-prohibitive to automate, but it’s generally a good principle to focus on.
  4. Follow the Single Level of Abstraction Principle (SLAP). A test case should do everything that it needs using data setup, utility and helper methods, but should make calls to them inline.

The Salesforce CPQ application is built on the Lightning Platform using many Sales Cloud features while seamlessly integrating with them. The CPQ application is made up of a UI backed by a powerful set of APIs that can be accessed using the SOAP or REST protocols. This way, you can break down your automation strategy into layers that map to the layers of the CPQ application.

Here are four kinds of automation tests that you can develop to throughly test Salesforce CPQ,

1) Apex tests

Apex tests run automatically before and after upgrades in the Apex test harness. In Salesforce CPQ, Apex tests can be used to validate contracting, ordering, amend and renewal flows along with any functionality based on SObject CRUD. UI and REST services are not covered during Apex.

Here’s a sample Apex test you can write to validate that Contracts get amended correctly. To do this we’ll create a ‘ContractAmender’ class like defined below. This class amends a contract using the Salesforce CPQ Contract API, but you could replace this with any custom logic. Also, before adding these classes, make sure you create the CPQ Model classes as described here.

/**
* Simple Class to use the ContractAPI to amend a contract
*/
public with sharing class ContractAmender {
    public QuoteModel amend(String contractId) {
        String quoteJSON = SBQQ.ServiceRouter.load('SBQQ.ContractManipulationAPI.ContractAmender', contractId, null);
        return (QuoteModel) JSON.deserialize(quoteJSON, QuoteModel.class);
    }
}
/**
 * Apex Test for ContractAmender
 */
@isTest
private class ContractAmenderTests {
    private static Account account;
    private static Product2[] products;

    /**
     * Amend a Contract, validate amendment quote - <br>
     * 1. Create a Contract with a Subscription and a Percent of Total Line <br>
     * 2. Amend the Contract <br>
     * 3. Verify the amendment quote is accurate <br>
     */
    @isTest static void testAmendContract() {
        // Create an account
        account = new Account(Name = 'Amazing Computers');
        insert new Account[] { account };

        // Create a Fixed Price subscription and a PoT product
        products = new List<Product2>();
        products.add(new Product2(Name = 'FixedPriceProduct', IsActive = true, SBQQ__SubscriptionPricing__c = 'Fixed Price', SBQQ__SubscriptionTerm__c = 12, SBQQ__SubscriptionType__c = 'Renewable'));
        products.add(new Product2(Name = 'PotProduct', IsActive = true, SBQQ__SubscriptionPricing__c = 'Percent Of Total', SBQQ__SubscriptionTerm__c = 12, SBQQ__SubscriptionType__c = 'Renewable'));
        insert products;

        // Create a Contract
        Contract contract = new Contract(AccountId = account.Id, SBQQ__PreserveBundleStructureUponRenewals__c = true, StartDate = Date.today());
        insert contract;

        // Add Percent Of Total Subscription to Contract
        Date endDate = Date.today().addYears(5);
        Integer pOTQty = 2;
        SBQQ__Subscription__c subscriptionPOT = new SBQQ__Subscription__c(SBQQ__Contract__c = contract.Id, SBQQ__Product__c = products[1].Id, SBQQ__Quantity__c = pOTQty, SBQQ__SubscriptionEndDate__c = endDate);
        insert subscriptionPOT;

        // Add Fixed Price Subscription to Contract
        Integer fPQty = 6;
        SBQQ__Subscription__c subscriptionFP = new SBQQ__Subscription__c(SBQQ__Contract__c = contract.Id, SBQQ__Product__c = products[0].Id, SBQQ__Quantity__c = fPQty, SBQQ__RequiredById__c = subscriptionPOT.Id, SBQQ__SubscriptionEndDate__c = endDate);
        insert subscriptionFP;

        // Amend Contract
        QuoteModel amendmentQuote = new ContractAmender().amend(contract.Id);

        // Verify
        System.assertEquals(2, amendmentQuote.lineItems.size(), 'Number of line items on amendment quote is not as expected');
    }
}

Have you earned your Apex Testing badge on Trailhead? Get it here!

2) CPQ API tests

Use your favorite test harnesses to write tests using the CPQ API. The API allows you to test the REST-based services. Use the CPQ API to test your quote calculate functionality, quote calculator plugins, document generation, and more. For example, you can write a test that reads a quote with product(s), calculates the quote and saves it.

Here’s a simple example in Apex that uses QuoteAPI.QuoteReader to test if quotes can be read.

/**
 * Test Class to validate CPQ functionality using the QuoteAPI
 */
@IsTest
private class QuoteAPITests {
    private static Account quoteApiAccount;
    private static Opportunity quoteApiOpp;
    private static SBQQ__Quote__c quoteApiQuoteRecord;

    // You can also create a full Model as described in the documentation and use that instead
    // https://developer.salesforce.com/docs/atlas.en-us.cpq_dev_api.meta/cpq_dev_api/cpq_api_models.htm
    private class TinyQuoteModel {
        public SBQQ__Quote__c record;
        public TinyQuoteLineModel[] lineItems;
    }

    // You can also create a full Model as described in the documentation and use that instead
    // https://developer.salesforce.com/docs/atlas.en-us.cpq_dev_api.meta/cpq_dev_api/cpq_api_models.htm
    private class TinyQuoteLineModel {
        public SBQQ__QuoteLine__c record;
    }

    // Create an Account, Opportunity and a Quote
    private static void setUpQuoteWithNoLines() {
        quoteApiAccount = new Account(Name='Amazing Computers');
        insert quoteApiAccount;

        quoteApiOpp = new Opportunity(AccountId=quoteApiAccount.Id,Name='ComputerMaintenance1K',CloseDate=System.today().addMonths(12),StageName='Prospecting',Pricebook2Id=Test.getStandardPricebookId());
        insert quoteApiOpp;

        quoteApiQuoteRecord = new SBQQ__Quote__c(SBQQ__Opportunity2__c=quoteApiOpp.Id,SBQQ__StartDate__c=System.today(),SBQQ__SubscriptionTerm__c=12);
        insert quoteApiQuoteRecord;
    }

    /**
     * Read a quote created in setUpQuoteWithNoLines using the QuoteReader API
     */
    @isTest static void loadQuoteByIdReturnsEmptyQuote() {
        // Create Quote
        setUpQuoteWithNoLines();
        String qid = (String)quoteApiQuoteRecord.Id;

        // Read quote using QuoteAPI.QuoteReader
        Test.startTest();
        String qmodelJson = SBQQ.ServiceRouter.read('QuoteAPI.QuoteReader', qid);
        TinyQuoteModel quote = (TinyQuoteModel)JSON.deserialize(qmodelJson, TinyQuoteModel.class);
        Test.stopTest();

        // Verify
        System.assert(quote != null, 'Did not load quote, QuoteReader returned null');
        System.assertEquals(quoteApiQuoteRecord.Id, quote.record.get('Id'), 'Quote Id is not the one expected');
        System.assertEquals(0, quote.lineItems.size(), 'Quote line count is not 0');
    }
}

Learn more about the CPQ API here!

Tip: You can also write tests in Java using Junit or TestNG as a test harness and a framework like Rest-Assured. If your team is more comfortable with JS, Frisby is a useful REST API testing framework.

3) Selenium Webdriver UI tests

Use your favorite test harness to write tests using the Selenium framework to automate browser actions. Because Selenium tests are expensive to run and maintain, they should be used only for critical flows. We recommend using Selenium tests for client-side features like option constraints and end-to-end flows. Note that Selenium tests are subject to modification when the UI changes. The Page Object pattern encapsulates all the locators and operations one can do with them in to a single class. Use it to reduce the cost of test maintenance and to improve readability. Check out this reference on Selenium HQ to learn more about PageObjects.

Here’s a sample Page Object written in Java using Selenium Webdriver for the CPQ QuoteLineEditor:

/**
 * Page Object to represent the CPQ QuoteLineEditor <br>
 * All interaction with the QuoteLineEditor needs to happen here <br>
 * A Test has to always go through a PageObject to access a Page / Elements on the App <br>
 */
public class CpqQuoteLineEditorPage {
    static final String BUTTONS_PREFIX = "//paper-button[text()='%s']";
    static final String TABLE_CELL_PREFIX = "//sf-le-table-cell[@item='%s']";
    static final String TABLE_CELL_INPUT = "/descendant::sb-input/input";
    static final String LOADING_SPINNER = "//div[@class='sbLoadingMask']";
    static final String LINE_EDITOR = "sb-line-editor";
    
    public CpqQuoteLineEditor() {
    // Do not instantiate object before page is fully loaded
        waitForSpinnerToDisappear();
    }

    /**
     * Buttons available on QuoteLineEditor
     */
    public enum QuoteLineEditorButtons {
        ADD_PRODUCTS("Add Products"), SAVE("Save"), QUICK_SAVE("Quick Save"), CANCEL("Cancel"), CALCULATE(
                "Calculate"), DELETE_LINES("Delete Lines"), ADD_GROUPS("Add Groups");

        private final String element;

        QuoteLineEditorButtons(String name) {
            this.element = name;
        }

        public String getName() {
            return element;
        }
    }

    /**
     * Click any button represented by the QuoteLineEditorButtons enum 
     * @param, button, 
     *             UI button on the QuoteLineEditor
     */
    public void clickButton(QuoteLineEditorButtons button) {
        // webDriverUtil is a Class that is a wrapper around standard WebDriver
        // functions like getElement(), click() etc.
        WebElement buttonElement = webDriverUtil.getWebDriverWait().until(
                ExpectedConditions.elementToBeClickable(webDriverUtil.getVisibleWebElement(By.tagName(LINE_EDITOR))
                        .findElement(By.xpath(String.format(BUTTONS_PREFIX, button.getName())))));
        buttonElement.click();
    }
    
    /**
     * Wait until the loading spinner disappears
     */
    private void waitForSpinnerToDisappear() {
        webDriverUtil.waitUntilInvisibility(By.xpath(LOADING_SPINNER));
    }
}

Use the “CpqQuoteLineEditorPage” PageObject as shown below:

// Navigate to the QuoteLineEditor
...
// Instantiate the CpqQuoteLineEditorPage PageObject
CpqQuoteLineEditorPage quoteLineEditorPage = new CpqQuoteLineEditorPage();

quoteLineEditorPage.clickButton(CpqQuoteLineEditorPage.QuoteLineEditorButtons.ADD_PRODUCTS);
...
// Calculate the quote
quoteLineEditorPage.clickButton(QuoteLineEditorPage.QuoteLineEditorButtons.CALCULATE);
...
// Save the Quote
quoteLineEditorPage.clickButton(QuoteLineEditorPage.QuoteLineEditorButtons.SAVE);

Tip: Write your tests in Java with Junit or TestNG as a harness and use Selenium / Webdriver to interact with the browser. If you’re more familiar with JavaScript, Mocha with Selenium / Webdriver is also a great option. Selenium provides clients in a number of languages like Perl, Python, Ruby, C# etc.

4) Lightning Component tests

If you’re writing custom Lightning components, the Lightning Testing Service (or LTS) is a set of tools and services that let you create test suites for your Lightning components using standard JavaScript test frameworks, such as Jasmine and Mocha. LTS has many great features built in, so you can jump right in to writing tests for your components. The GitHub repository above has a lot of great code samples. If you don’t write any custom lightning component, you won’t have any tests in this category. If you’re already using other component-based frameworks like Polymer or React, consider writing tests using their testing frameworks.

Ready to get started?

We recommend running your automation against Salesforce DX scratch orgs. The Package Development Model Trailhead module is a great place to get started with scratch orgs.

I hope this post helped you learn more about about the ways you can automate your testing of Salesforce CPQ. If you have any questions or thoughts, feel free to leave a comment below or Tweet me at @NAthmaraman.

Leave your comments...

Automating Salesforce CPQ Testing