Overview
In this tutorial, we will implement an Apex application to create a simple business process in Salesforce, a well-known cloud-based CRM.
Apex is an object-oriented programming language, similar to Java, allowing developers to customize Salesforce to their needs; the code is deployed to, compiles and runs in the cloud.
Apex custom applications (code and automated tests) can be developed entirely using a browser and the developer console.
The developer console provides a complete environment for editing and debugging code, including the ability to perform queries in the database. It also allows the implementation of automated tests, running them and seeing their results.
Users and developers can either use SOQL or SOSL to extract information from the DB.
However, in a CI/CD environment, code will be edited elsewhere (e.g. using an IDE such as VSCode) and managed by some VCS (e.g. Git).
The Salesforce CLI (sfdx) is used to interact with Salesforce platform, mainly for deploying code and triggering the test runs. Results can be collected from Salesforce and processed by the CI tool and even sent to a central test management solution like Xray.
Use case
In this tutorial, we will implement a Salesforce application which aims to create an "order" whenever an "opportunity" is transitioned to a closed state if certain minimum conditions are met.
Some fields are copied from the opportunity to the new "order" object.
Implementation using VS Code and "sfdx" CLI tool
Requirements
We start by creating an Apex project with all the necessary files. In VSCode this is straightforward.
We need to review the project configuration file, mainly the instance URL and the API version supported by our application.
{ "packageDirectories": [ { "path": "force-app", "default": true } ], "namespace": "", "sfdcLoginUrl": "https://test.salesforce.com", "sourceApiVersion": "48.0" }
We then can proceed to create the application code and corresponding tests.
Our application is composed of a trigger (OpportunityTrigger), helper (OpportunityTriggerHelper) and the test class (OpportunityTriggerTest).
We will implement two basic tests: one that checks the successful case, where the "order" is created, and another for checking invalid data.
Asserts are provided by the System class.
The test class should have the @isTest annotation and so the test methods. Previous versions used the "testMethod" classifier in the method statement but the annotation is more clear.
Each test runs in a transaction, thus any object that is created within the test is discarded when it finishes.
@isTest public class OpportunityTriggerTest { @isTest public static void testValidOpportunity(){ //Account the will be related with the Opportunities Account acc = new Account(); acc.Name = 'Test Account'; insert acc; Id pricebookId = Test.getStandardPricebookId(); List <Opportunity> oppsToInsert = new List <Opportunity>(); //Opportunity to insert a related Order Opportunity opp = new Opportunity(); String temp_opportunity_name = 'Test Opportunity'; opp.Name = temp_opportunity_name; opp.AccountId = acc.Id; opp.CloseDate = system.today(); opp.Probability = 70; opp.StageName = 'Closed Won'; opp.Amount = 15000; opp.Pricebook2Id = pricebookId; oppsToInsert.add(opp); insert oppsToInsert; Opportunity lastOpp; Opportunity[] opps = [SELECT Id, Amount FROM Opportunity WHERE OwnerId = :UserInfo.getUserId() and name = :temp_opportunity_name]; System.assert((opps.size() > 0),'no opportunity was found'); lastOpp = opps[0]; Order order = [SELECT Id, Status, Amount__c from order where Opportunity.Name = :temp_opportunity_name]; System.assert(order != NULL); System.assertEquals(opp.Amount, lastOpp.Amount); System.assertEquals(opp.Amount, order.Amount__c); System.assertEquals(order.Status, 'Draft'); } @isTest public static void testInvalidOpportunity(){ //Account the will be related with the Opportunities Account acc = new Account(); acc.Name = 'Test Account'; insert acc; Id pricebookId = Test.getStandardPricebookId(); List <Opportunity> oppsToInsert = new List <Opportunity>(); //Opportunity to hit the validation rule Opportunity opp1 = new Opportunity(); opp1.Name = 'Test Opportunity1'; opp1.AccountId = acc.Id; opp1.CloseDate = system.today(); opp1.Probability = 70; opp1.StageName = 'Closed Won'; opp1.Amount = 0; oppsToInsert.add(opp1); //Check that the Opportunity hit the validation rule try{ insert oppsToInsert; }catch (exception e){ system.assert(e.getMessage().contains('To save this Opportunity as Close Won you need to fill the Pricebook field and the Amount needs to be greater than 0')); } } }
After the code and tests are implemented, they need to be deployed to Salesforce using the sfdx
tool. However, before being able to use any of sfdx
commands, we need to authenticate it with Salesforce.
sfdx force:auth:web:login -r https://test.salesforce.com -a TestOrg1
This will store some tokens under the folder $HOME/.sfdx/
, which will be used for subsequent sfdx
commands.
We can also give an alias to the Salesforce organization we are connecting to. It's import to use the correct instance URL (in this case we're using the "test.salesforce.com" instance).
After authentication is done, we can deploy our application.
sfdx force:source:deploy -p force-app --json --loglevel fatal -u TestOrg1
Whenever an application code is deployed, tests may also run automatically; this depends on the configuration and on the kind of environment where the code has been deployed to (more info here).
However, we may control and enforce which tests to run, based on the name of the test suite or of the test classes. The creation of a JUnit XML report is also possible.
Note that tests will run remotely, thus any change to the application code or tests needs to be deployed beforehand.
sfdx force:apex:test:run --resultformat human --resultformat junit --outputdir reports --testlevel RunSpecifiedTests -n OpportunityTriggerTest -u TestOrg1 > reports/junit.xml
After successfully running tests and generating the JUnit XML report (e.g. junit.xml ), it can be imported to Xray (either by the REST API or by using one of the available CI addons or even through Import Execution Results action within the Test Execution).
Each check is mapped to a Generic Test in Jira, and the Generic Test Definition field contains the name of the test class followed by the name of the test method.
The Context section contains information about the log/assertion.
Implementation using "ant migration" tool
Requirements
In this scenario, we're using the Ant Migration Tool to manage and deploy code changes to Salesforce.
This tailored version of ant provides some specific Salesforce tasks to manage the deployment of our applications.
However, as of version 47.0 of this tool, it does not provide a way to generate JUnit XML reports. Fortunately, there is an open-source project ("force-deploy-with-xml-report-task") that is able to extend "ant" to provide this capability.
You need to install the ant-salesforce.jar obtained from Salesforce to your maven repository.
mvn install:install-file -Dfile=ant-salesforce.jar -DgroupId=com.force.api -DartifactId=ant-salesforce -Dversion=47.0.0 -Dpackaging=jar
Then you can build "force-deploy-with-xml-report-task", after updating it's pom.xml to reflect the version of the downloaded ant-salesforce.jar.
The file structure for our application can be based on the "sample" project provided in the zip file of the Ant Migration Tool.
We can use the code provided in the previous section and organize it as follows.
. ├── build.properties ├── build.xml ├── codepkg │ ├── classes │ │ ├── OpportunityTriggerHelper.cls │ │ ├── OpportunityTriggerHelper.cls-meta.xml │ │ ├── OpportunityTriggerTest.cls │ │ └── OpportunityTriggerTest.cls-meta.xml │ ├── package.xml │ └── triggers │ ├── OpportunityTrigger.trigger │ └── OpportunityTrigger.trigger-meta.xml ├── lib │ ├── ant-salesforce.jar │ └── force-deploy-with-xml-report-task.jar ├── mypkg │ └── objects ├── package.xml ├── removecodepkg │ ├── destructiveChanges.xml │ └── package.xml ├── reports │ └── TEST-Apex.xml └── unpackaged └── package.xml
The root build.xml file needs to be updated to produce the JUnit XML report; this is done in the deployCode task (we kept the original deployCode task just for comparison purpose).
Code can be deployed and tests run; rollback is performed if tests fail.
$ ant deployCode Buildfile: /Users/smsf/exps/salesforce/create_order_from_opportunity_using_ant/create_order_from_opportunity/build.xml deployCode: [delete] Deleting directory /Users/smsf/exps/salesforce/create_order_from_opportunity_using_ant/create_order_from_opportunity/reports [sfdeploy] Request for a deploy submitted successfully. [sfdeploy] Request ID for the current deploy task: 0Af3N000005MfsoSAC [sfdeploy] Waiting for server to finish processing the request... [sfdeploy] Request Status: Pending [sfdeploy] Request Status: Pending [sfdeploy] Request Status: Failed BUILD FAILED /Users/smsf/exps/salesforce/create_order_from_opportunity_using_ant/create_order_from_opportunity/build.xml:74: Failures: Test failure, method: OpportunityTriggerTest.testValidOpportunity -- System.AssertException: Assertion Failed: Expected: 15000, Actual: 14999.00 stack Class.OpportunityTriggerTest.testValidOpportunity: line 38, column 1 There are 6 flows that have no coverage: - CancelEvent - CreateEvent - License_Booleans_Flow - License_Process - OnCalendlyActionCreated - Parent_Booleans_Flow Total time: 26 seconds
The JUnit XML report is stored under the reports/
folder. It can then be submitted to Xray using the REST API, the UI or using one of the available CI addons.
Checking it live
After deploying and testing your code, you may see it live.
- Start by clicking on the Opportunities Tab. You will be redirected to the list/table view of that object.
- To create a new Opportunity, just click on the button named “New”;
- Fill the opportunity related fields.
- For the process to run, you need to fill certain fields, but only if the “Stage” field has the value of “Closed won”, if so, you need to fill the fields “Price Book” and the “Amount” or you will be presented with a validation rule. If you try to save without those fields filled, you will be presented with an error message as you can see below.
- After filling the mandatory fields for the process to run, you will be able to save the Opportunity, and an Order will be created which will be related to this Opportunity.
- Finally, you can check the Order record and see that the relevant Info. regarding the Opportunity will be passed to the Order, as you can see in the image below.
Tips
Check authenticated organizations with the CLI tool
You can quickly assess the connected/authenticated
$ sfdx force:org:list === Orgs ALIAS USERNAME ORG ID CONNECTED STATUS ──────── ───────────────────────────────────── ────────────────── ──────────────── TestOrg1 xxxxxxxxxxxxxx@xpand-it.com.hbsandbox 00D3N0000008oIsUAI Connected
Running tests from within VSCode
It's possible to trigger the running of tests from VSCode; however, updated code must be deployed first to the Salesforce organization.
References
- https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_dev_guide.htm
- https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_testing.htm
- https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_reference.htm
- https://developer.salesforce.com/tools/sfdxcli
- https://www.tutorialspoint.com/apex/apex_testing.htm
- SOQL vs SOSL
- https://trailhead.salesforce.com/en/content/learn/modules/apex_testing
- https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_qs_HelloWorld.htm
- https://developer.salesforce.com/docs/atlas.en-us.daas.meta/daas/commondeploymentissues.htm
- https://github.com/beamso/force-deploy-with-xml-report-task