DEV Community

Cover image for How to Query Salesforce Code Coverage
Matheus Goncalves
Matheus Goncalves

Posted on • Edited on

6

How to Query Salesforce Code Coverage

In Salesforce Orgs, code coverage percentage is a calculation of the number of covered lines divided by the sum of the number of covered lines and uncovered lines.

In addition to ensuring the quality of your code, unit tests enable you to meet the code coverage requirements for deploying or packaging Apex. To deploy Apex or package it for the Salesforce AppExchange, unit tests must cover at least 75% of your Apex code, and those tests must pass.

After running tests, you can view code coverage information in the Tests tab of the Developer Console. The code coverage pane includes coverage information for each Apex class and the overall coverage for all Apex code in your organization.

However, you can also use SOQL queries with Tooling API as an alternative way of checking code coverage and a quick way to get more details. Let's see how it works:

Inspecting Code Coverage

Step 1 - Get your org's base URL:

If you know what's your Org's base URL (you can get it from your browser as well), please jump to step 2.

  • From Developer Console, open the Debug menu, then select the option "Open Execute Anonymous Window (CTRL+E)
  • Enter the following Apex Code:

    String baseURL =  'https://' + System.URL.getSalesforceBaseUrl().getHost();
    System.debug(baseURL);

Enter fullscreen mode Exit fullscreen mode
  • Click the button [Execute]
  • Go to your Logs tab on the bottom of the page
  • Copy the full Base URL, including https://

Step 2 - Now you know your base URL, create a Remote Site Setting

  • From Setup, enter Remote Site Settings in the Quick Find box, then select Remote Site Settings.
  • Click New Remote Site.
  • Enter a descriptive term for the Remote Site Name.
  • Enter the URL for the remote site.
  • Optionally, enter a description of the site.

  • Click Save.
  • Just in case, create a second Remote Site Settings for the URL in the "Visual.force.com" format:
    • https://c.xx99.visual.force.com - where xx99 is the code of your org's instance.

Step 3 - Get the JSON file for the Code Coverage Wrapper

  • Go back to the Developer Console, open the Debug menu, then select the option "Open Execute Anonymous Window (CTRL+E)
  • Enter the following Apex Code:
String baseURL = 'https://' + System.URL.getSalesforceBaseUrl().getHost();
string queryStr = 'SELECT+NumLinesCovered,ApexClassOrTriggerId,ApexClassOrTrigger.Name,NumLinesUncovered,Coverage+FROM+ApexCodeCoverageAggregate';
String ENDPOINT = baseURL + '/services/data/v40.0/tooling/';
HttpRequest req = new HttpRequest();
req.setEndpoint(ENDPOINT + 'query/?q=' + queryStr);
req.setHeader('Authorization', 'Bearer ' + UserInfo.getSessionID());
req.setHeader('Content-Type', 'application/json');
req.setMethod('GET');
req.setTimeout(80000);
Http http = new Http();
HTTPResponse res = http.send(req);
System.debug(res.getBody());
  • Click the button [Execute]
  • Go to your Logs tab on the bottom of the page
  • Copy the full JSON result
    • Please note you will need to remove any text before the first "{" bracket.
    • Eg.: "hh:mm:ss:999 USER_DEBUG [99]|DEBUG|"
  • Go to the website JSON2Apex: https://json2apex.herokuapp.com/
  • Paste your JSON file
  • Enter the name for the generated class (here, we will use CodeCoverageWrapper)
  • Click the button [Create Apex]

  • Download the generated zip file that contains the Wrapper class plus the Test class.
  • Extract the files
  • Open the files with your prefered text editor
  • Check if everything looks good, and add these classes to your Salesforce org as new classes.

This is how it looks like:

public class CodeCoverageWrapper {
public class ApexClassOrTrigger {
public Attributes attributes {get;set;}
public String Name {get;set;}
public ApexClassOrTrigger(JSONParser parser) {
while (parser.nextToken() != System.JSONToken.END_OBJECT) {
if (parser.getCurrentToken() == System.JSONToken.FIELD_NAME) {
String text = parser.getText();
if (parser.nextToken() != System.JSONToken.VALUE_NULL) {
if (text == 'attributes') {
attributes = new Attributes(parser);
} else if (text == 'Name') {
Name = parser.getText();
} else {
System.debug(LoggingLevel.WARN, 'ApexClassOrTrigger consuming unrecognized property: '+text);
consumeObject(parser);
}
}
}
}
}
}
public Integer size {get;set;}
public Integer totalSize {get;set;}
public Boolean done {get;set;}
public Object queryLocator {get;set;}
public String entityTypeName {get;set;}
public List<Records> records {get;set;}
public CodeCoverageWrapper(JSONParser parser) {
while (parser.nextToken() != System.JSONToken.END_OBJECT) {
if (parser.getCurrentToken() == System.JSONToken.FIELD_NAME) {
String text = parser.getText();
if (parser.nextToken() != System.JSONToken.VALUE_NULL) {
if (text == 'size') {
size = parser.getIntegerValue();
} else if (text == 'totalSize') {
totalSize = parser.getIntegerValue();
} else if (text == 'done') {
done = parser.getBooleanValue();
} else if (text == 'queryLocator') {
queryLocator = parser.readValueAs(Object.class);
} else if (text == 'entityTypeName') {
entityTypeName = parser.getText();
} else if (text == 'records') {
records = arrayOfRecords(parser);
} else {
System.debug(LoggingLevel.WARN, 'CodeCoverageWrapper consuming unrecognized property: '+text);
consumeObject(parser);
}
}
}
}
}
public class Coverage_X {
public List<Integer> coveredLines {get;set;}
public List<CoveredLines> uncoveredLines {get;set;}
public Coverage_X(JSONParser parser) {
while (parser.nextToken() != System.JSONToken.END_OBJECT) {
if (parser.getCurrentToken() == System.JSONToken.FIELD_NAME) {
String text = parser.getText();
if (parser.nextToken() != System.JSONToken.VALUE_NULL) {
if (text == 'coveredLines') {
coveredLines = arrayOfInteger(parser);
} else if (text == 'uncoveredLines') {
uncoveredLines = arrayOfCoveredLines(parser);
} else {
System.debug(LoggingLevel.WARN, 'Coverage_X consuming unrecognized property: '+text);
consumeObject(parser);
}
}
}
}
}
}
public class Coverage_Y {
public List<CoveredLines> coveredLines {get;set;}
public List<Integer> uncoveredLines {get;set;}
public Coverage_Y(JSONParser parser) {
while (parser.nextToken() != System.JSONToken.END_OBJECT) {
if (parser.getCurrentToken() == System.JSONToken.FIELD_NAME) {
String text = parser.getText();
if (parser.nextToken() != System.JSONToken.VALUE_NULL) {
if (text == 'coveredLines') {
coveredLines = arrayOfCoveredLines(parser);
} else if (text == 'uncoveredLines') {
uncoveredLines = arrayOfInteger(parser);
} else {
System.debug(LoggingLevel.WARN, 'Coverage_Y consuming unrecognized property: '+text);
consumeObject(parser);
}
}
}
}
}
}
public class Coverage_Z {
public List<CoveredLines> coveredLines {get;set;}
public List<CoveredLines> uncoveredLines {get;set;}
public Coverage_Z(JSONParser parser) {
while (parser.nextToken() != System.JSONToken.END_OBJECT) {
if (parser.getCurrentToken() == System.JSONToken.FIELD_NAME) {
String text = parser.getText();
if (parser.nextToken() != System.JSONToken.VALUE_NULL) {
if (text == 'coveredLines') {
coveredLines = arrayOfCoveredLines(parser);
} else if (text == 'uncoveredLines') {
uncoveredLines = arrayOfCoveredLines(parser);
} else {
System.debug(LoggingLevel.WARN, 'Coverage_Z consuming unrecognized property: '+text);
consumeObject(parser);
}
}
}
}
}
}
public class Attributes {
public String type_Z {get;set;} // in json: type
public String url {get;set;}
public Attributes(JSONParser parser) {
while (parser.nextToken() != System.JSONToken.END_OBJECT) {
if (parser.getCurrentToken() == System.JSONToken.FIELD_NAME) {
String text = parser.getText();
if (parser.nextToken() != System.JSONToken.VALUE_NULL) {
if (text == 'type') {
type_Z = parser.getText();
} else if (text == 'url') {
url = parser.getText();
} else {
System.debug(LoggingLevel.WARN, 'Attributes consuming unrecognized property: '+text);
consumeObject(parser);
}
}
}
}
}
}
public class Coverage {
public List<Integer> coveredLines {get;set;}
public List<Integer> uncoveredLines {get;set;}
public Coverage(JSONParser parser) {
while (parser.nextToken() != System.JSONToken.END_OBJECT) {
if (parser.getCurrentToken() == System.JSONToken.FIELD_NAME) {
String text = parser.getText();
if (parser.nextToken() != System.JSONToken.VALUE_NULL) {
if (text == 'coveredLines') {
coveredLines = arrayOfInteger(parser);
} else if (text == 'uncoveredLines') {
uncoveredLines = arrayOfInteger(parser);
} else {
System.debug(LoggingLevel.WARN, 'Coverage consuming unrecognized property: '+text);
consumeObject(parser);
}
}
}
}
}
}
public class CoveredLines {
public CoveredLines(JSONParser parser) {
while (parser.nextToken() != System.JSONToken.END_OBJECT) {
if (parser.getCurrentToken() == System.JSONToken.FIELD_NAME) {
String text = parser.getText();
if (parser.nextToken() != System.JSONToken.VALUE_NULL) {
{
System.debug(LoggingLevel.WARN, 'CoveredLines consuming unrecognized property: '+text);
consumeObject(parser);
}
}
}
}
}
}
public class Records {
public Attributes attributes {get;set;}
public Integer NumLinesCovered {get;set;}
public String ApexClassOrTriggerId {get;set;}
public ApexClassOrTrigger ApexClassOrTrigger {get;set;}
public Integer NumLinesUncovered {get;set;}
public Coverage Coverage {get;set;}
public Records(JSONParser parser) {
while (parser.nextToken() != System.JSONToken.END_OBJECT) {
if (parser.getCurrentToken() == System.JSONToken.FIELD_NAME) {
String text = parser.getText();
if (parser.nextToken() != System.JSONToken.VALUE_NULL) {
if (text == 'attributes') {
attributes = new Attributes(parser);
} else if (text == 'NumLinesCovered') {
NumLinesCovered = parser.getIntegerValue();
} else if (text == 'ApexClassOrTriggerId') {
ApexClassOrTriggerId = parser.getText();
} else if (text == 'ApexClassOrTrigger') {
ApexClassOrTrigger = new ApexClassOrTrigger(parser);
} else if (text == 'NumLinesUncovered') {
NumLinesUncovered = parser.getIntegerValue();
} else if (text == 'Coverage') {
Coverage = new Coverage(parser);
} else {
System.debug(LoggingLevel.WARN, 'Records consuming unrecognized property: '+text);
consumeObject(parser);
}
}
}
}
}
}
public static CodeCoverageWrapper parse(String json) {
System.JSONParser parser = System.JSON.createParser(json);
return new CodeCoverageWrapper(parser);
}
public static void consumeObject(System.JSONParser parser) {
Integer depth = 0;
do {
System.JSONToken curr = parser.getCurrentToken();
if (curr == System.JSONToken.START_OBJECT ||
curr == System.JSONToken.START_ARRAY) {
depth++;
} else if (curr == System.JSONToken.END_OBJECT ||
curr == System.JSONToken.END_ARRAY) {
depth--;
}
} while (depth > 0 && parser.nextToken() != null);
}
private static List<Integer> arrayOfInteger(System.JSONParser p) {
List<Integer> res = new List<Integer>();
if (p.getCurrentToken() == null) p.nextToken();
while (p.nextToken() != System.JSONToken.END_ARRAY) {
res.add(p.getIntegerValue());
}
return res;
}
private static List<Records> arrayOfRecords(System.JSONParser p) {
List<Records> res = new List<Records>();
if (p.getCurrentToken() == null) p.nextToken();
while (p.nextToken() != System.JSONToken.END_ARRAY) {
res.add(new Records(p));
}
return res;
}
private static List<CoveredLines> arrayOfCoveredLines(System.JSONParser p) {
List<CoveredLines> res = new List<CoveredLines>();
if (p.getCurrentToken() == null) p.nextToken();
while (p.nextToken() != System.JSONToken.END_ARRAY) {
res.add(new CoveredLines(p));
}
return res;
}
}

Step 4 - Create a Helper Class

  • Create the following class, that contains a method that will query and return information about the Code Coverage
public class CodeCoverageHelper {
//This method will return a Map of Classes Names and the respective Code Coverage
public static Map<String, Decimal> getCodeCoverage() {
Map<String, Decimal> resultMap = new Map<String, Decimal>();
string queryStr = 'SELECT+NumLinesCovered,ApexClassOrTriggerId,ApexClassOrTrigger.Name,NumLinesUncovered,Coverage+FROM+ApexCodeCoverageAggregate+ORDER+BY+ApexClassOrTrigger.Name';
String ENDPOINT = 'https://' + System.URL.getSalesforceBaseUrl().getHost() + '/services/data/v40.0/tooling/';
HttpRequest req = new HttpRequest();
req.setEndpoint(ENDPOINT + 'query/?q=' + queryStr);
req.setHeader('Authorization', 'Bearer ' + UserInfo.getSessionID());
req.setHeader('Content-Type', 'application/json');
req.setMethod('GET');
req.setTimeout(80000);
Http http = new Http();
HTTPResponse res = http.send(req);
if (res.getStatusCode() == 200) {
CodeCoverageWrapper codeCoverageWrapper = CodeCoverageWrapper.parse(res.getBody());
for(CodeCoverageWrapper.Records records : codeCoverageWrapper.records) {
String classOrTriggerName = records.ApexClassOrTrigger.Name;
Decimal numLinesCovered = records.NumLinesCovered;
Decimal numLinesUncovered = records.NumLinesUncovered;
Decimal totalNumberOfLines = numLinesCovered + numLinesUncovered;
if(totalNumberOfLines == 0) continue;
Decimal coveragePercentage = (numLinesCovered / totalNumberOfLines) * 100;
resultMap.put(classOrTriggerName, coveragePercentage);
}
}
return resultMap;
}
// Method created to sort the Map of Coverage values in Descending Order
public static Map<String, Decimal> sortCodeCoverageMapByCoverage(Map<String, Decimal> coverageMap) {
CoverageWrapper[] coverageList = new CoverageWrapper[]{};
for(String key : coverageMap.keySet()) {
coverageList.add(new CoverageWrapper(key, coverageMap.get(key)));
}
coverageList.sort();
CoverageWrapper[] finalList = new CoverageWrapper[]{};
for(Integer i = coverageList.size() -1; i >= 0; i = i-1 ) {
finalList.add(coverageList.get(i));
}
Map<String,Decimal> coverageToNameMap = new Map<String,Decimal>();
for(CoverageWrapper coverage : finalList) {
coverageToNameMap.put(coverage.getObjectName(), coverage.getValue());
}
return coverageToNameMap;
}
public static String buildCodeCoverageMessage(Decimal coverage, String objectName) {
String coverageMessage = '';
if(coverage < 10) {
coverageMessage = coverageMessage + ICON_ERROR + ' ' + MESSAGE_UNDER_10 + ' threshold';
}
if(coverage >= 10 && coverage < 75) {
coverageMessage = coverageMessage + ICON_WARNING + ' ' + MESSAGE_UNDER_75 + ' threshold';
}
if(coverage >= 75) {
coverageMessage = coverageMessage + ICON_OK + ' ' + MESSAGE_ABOVE_75 + ' threshold';
}
coverageMessage = coverageMessage + ' | Code Coverage for [ ' + objectName + ' ]: ' + coverage + '%';
return coverageMessage;
}
public static final String ICON_ERROR = '⛔';
public static final String ICON_WARNING = '️⚠️';
public static final String ICON_OK = '✅';
public static final String MESSAGE_UNDER_10 = 'Under the 10%';
public static final String MESSAGE_UNDER_75 = 'Under the 75%';
public static final String MESSAGE_ABOVE_75 = 'Above the 75%';
public class CoverageWrapper implements Comparable {
private Decimal coverageValue{get; set;}
private String objectName {get; set;}
private Integer intValue {get; set;}
CoverageWrapper(String objectName, Decimal coverageValue) {
this.objectName = objectName;
this.coverageValue = coverageValue;
this.intValue = coverageValue.intValue();
}
public Decimal getValue() {
return this.coverageValue;
}
public String getObjectName() {
return this.objectName;
}
public Integer compareTo(Object other) {
return intValue-((CoverageWrapper)other).intValue;
}
}
}

Don't forget to write the test class for it.

Step 5 - Create a Controller and a Page to display the Code Coverage

  • Create the following class, that contains the logic for the page
public class CodeCoverageController {
public String messageUnder10 {
get { return CodeCoverageHelper.MESSAGE_UNDER_10; }
private set;
}
public String messageUnder75 {
get { return CodeCoverageHelper.MESSAGE_UNDER_75; }
private set;
}
public String messageAbove75 {
get { return CodeCoverageHelper.MESSAGE_ABOVE_75; }
private set;
}
public String[] codeCoverageMessages {
get {
if(codeCoverageMessages == null) codeCoverageMessages = new String[]{};
return codeCoverageMessages;
}
set;
}
public Map<String, Decimal> codeCoverageMap {
get {
if(codeCoverageMap == null || codeCoverageMap.isEmpty()) {
codeCoverageMap = CodeCoverageHelper.getCodeCoverage();
}
return codeCoverageMap;
}
set;
}
public CodeCoverageController() {
populateCodeCoverageByName();
}
public void populateCodeCoverageByName() {
Map<String, Decimal> coverageMap = codeCoverageMap;
populateCodeCoverageInfo(coverageMap);
}
public void populateCodeCoverageByCoverage() {
Map<String, Decimal> coverageMap = CodeCoverageHelper.sortCodeCoverageMapByCoverage(codeCoverageMap);
populateCodeCoverageInfo(coverageMap);
}
public void populateCodeCoverageInfo(Map<String, Decimal> coverageMap){
codeCoverageMessages.clear();
for(String className : coverageMap.keySet()) {
Decimal coverage = coverageMap.get(className);
coverage = coverage.setScale(2);
String coverageMessage = CodeCoverageHelper.buildCodeCoverageMessage(coverage, className);
codeCoverageMessages.add(coverageMessage);
}
}
}

Create the following page, using the controller we just created.

Please note we're leveraging Salesforce Lightning Design System;

<apex:page controller="CodeCoverageController" showHeader="true" sidebar="false" docType="html-5.0">
<apex:slds />
<apex:form id="codeCoverageForm">
<apex:outputPanel id="codeCoverageRepeat" style="justify-content: left;">
<br/>
<div class="centered">
<div class="slds-text-heading_large">
<span>
<h2 style="justify-content: center;">Code Coverage information: </h2>
</span>
</div>
</div>
<br/>
<apex:outputPanel id="buttonsPanel">
<div class="text-center">
<apex:outputPanel>
<apex:commandButton action="{!populateCodeCoverageByName}" value="Sort by Name" reRender="codeCoverageForm" styleClass="slds-button slds-button_outline-brand slds-button--neutral slds-not-selected" />
</apex:outputPanel>
<span> or </span>
<apex:outputPanel>
<apex:commandButton action="{!populateCodeCoverageByCoverage}" value="Sort by Coverage" reRender="codeCoverageForm" styleClass="slds-button slds-button_outline-brand slds-button--neutral slds-not-selected" />
</apex:outputPanel>
</div>
</apex:outputPanel>
<br/>
<apex:repeat value="{!codeCoverageMessages}" var="codeCoverageMessage">
<!-- Code coverage is below 10% -->
<apex:outputPanel rendered="{!if(contains(codeCoverageMessage,messageUnder10),'true','false')}" style="justify-content: left;">
<div class="slds-notify slds-notify_alert slds-theme_alert-texture slds-theme_error" role="alert" style="justify-content: left;">
<span class="slds-assistive-text">error</span>
<h2 style="justify-content: left;">{!codeCoverageMessage}</h2>
</div>
</apex:outputPanel>
<!-- Code coverage is under 75% -->
<apex:outputPanel rendered="{!if(contains(codeCoverageMessage,messageUnder75),'true','false')}" style="justify-content: left;">
<div class="slds-notify slds-notify_alert slds-theme_alert-texture slds-theme_warning" role="alert" style="justify-content: left;">
<span class="slds-assistive-text">warning</span>
<h2 style="justify-content: left;">{!codeCoverageMessage}</h2>
</div>
</apex:outputPanel>
<!-- Code coverage is above 75% -->
<apex:outputPanel rendered="{!if(contains(codeCoverageMessage,messageAbove75),'true','false')}" style="justify-content: left;">
<div class="slds-notify slds-notify_alert slds-theme_alert-texture slds-theme_info" role="alert" style="justify-content: left;">
<span class="slds-assistive-text">info</span>
<h2 style="justify-content: left;">{!codeCoverageMessage}</h2>
</div>
</apex:outputPanel>
</apex:repeat>
</apex:outputPanel>
</apex:form>
</apex:page>

The page has two buttons. One to sort the results by Class / Trigger name, and another button to sort the results by Code Coverage.

This is how it looks like:

Final Considerations

You can follow the same logic to use the Tooling API whenever you need to query exposed metadata used in developer tooling that you can access through REST or SOAP.

Also, the method sortCodeCoverageMapByCoverage from CodeCoverageHelper is quite handy and can be used as an example for situations when you need to sort Maps by its values. In this case, sort the Code Coverage percentage value.

Just don't forget to adapt the Wrapper and the Comparable.

You can find all code used for this project in my GitHub account, at github.com/gitmatheus/QueryCodeCoverage

GitHub logo gitmatheus / QueryCodeCoverage

How to Query Code Coverage - After running tests, you can view code coverage information in the Tests tab of the Developer Console. The code coverage pane includes coverage information for each Apex class and the overall coverage for all Apex code in your organization. However, you can also use SOQL queries with Tooling API as an alternative way of checking code coverage and a quick way to get more details.

How to Query Code Coverage

In Salesforce Orgs, code coverage percentage is a calculation of the number of covered lines divided by the sum of the number of covered lines and uncovered lines. In addition to ensuring the quality of your code, unit tests enable you to meet the code coverage requirements for deploying or packaging Apex.

To deploy Apex or package it for the Salesforce AppExchange, unit tests must cover at least 75% of your Apex code, and those tests must pass. After running tests, you can view code coverage information in the Tests tab of the Developer Console. The code coverage pane includes coverage information for each Apex class and the overall coverage for all Apex code in your organization. However, you can also use SOQL queries with Tooling API as an alternative way of checking code coverage and a quick way to get more details.

The code…

I hope you enjoyed the ride and learned something new today.

See you next time.

This article was originally posted at matheus.dev

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs