I build a custom UI using LWC. The LWC allows you to , run the selected tests,
- select which tests you would like to run together
- run the selected tests
- gets the code coverage of each test and averages them together.
- and records the information on a custom object.
All of the above is working accept the linesCovered, linesUncovered are all 0. The tests that I’m running are all over 80% when ran in the developer console. I believe there is either something wrong with how I’m calculating the code coverage, or the json is not deserializing properly.
I used several debug logs in my Apex and error handling in my code and got back these results: 08:46:17:582 USER_DEBUG [198]|DEBUG|Coverage Data - Name: NAICSCodeUpdateTest, Covered: 0, Uncovered: 0, Percent: 0.0
08:46:17:583 USER_DEBUG [111]|DEBUG|Fetched coverage results: (CoverageData:[className=NAICSCodeUpdateTest, coveragePercentage=0.0, numLinesCovered=0, numLinesUncovered=0]) 08:46:17:583 USER_DEBUG [130]|DEBUG|Calculated total coverage: 0.0
08:46:17:617 USER_DEBUG [136]|DEBUG|Updated Test_Result__c with Coverage_Percent__c: Test_Result__c:{Id=a0EVZ000000FLfF2AW, JobId__c=1721738754256, TestClassIds__c=[“01p7h000002RSlbAAG”], Coverage_Percent__c=0.0}
@RestResource(urlmapping='/TestResultsController2')
global with sharing class TestResultsController2 {
public class CoverageData {
@AuraEnabled public String className { get; set; }
@AuraEnabled public Integer numLinesCovered { get; set; }
@AuraEnabled public Integer numLinesUncovered { get; set; }
@AuraEnabled public Double coveragePercentage { get; set; }
public CoverageData(String className, Integer numLinesCovered, Integer numLinesUncovered, Double coveragePercentage) {
this.className = className;
this.numLinesCovered = numLinesCovered;
this.numLinesUncovered = numLinesUncovered;
this.coveragePercentage = coveragePercentage;
}
}
@AuraEnabled(cacheable=true)
global static List<ApexClass> getTestClasses() {
String searchTerm = '%Test%';
return [SELECT Id, Name FROM ApexClass WHERE NamespacePrefix = null AND (Name LIKE :searchTerm OR Name LIKE :('%' + searchTerm))];
}
@HttpPost
@AuraEnabled
global static String runTests(List<String> selectedTestIds) {
List<ApexTestQueueItem> queueItems = new List<ApexTestQueueItem>();
List<ApexClass> selectedTestClasses = [SELECT Id, Name FROM ApexClass WHERE Id IN :selectedTestIds];
for (ApexClass cls : selectedTestClasses) {
queueItems.add(new ApexTestQueueItem(ApexClassId = cls.Id));
}
insert queueItems;
// Return a unique job ID to track the test run
String jobId = String.valueOf(System.currentTimeMillis());
Test_Result__c job = new Test_Result__c(JobId__c = jobId, TestClassIds__c = JSON.serialize(selectedTestIds));
insert job;
return jobId;
}
@AuraEnabled
global static Map<String, Object> getTestResults(String jobId) {
System.debug('Entering getTestResults with jobId: ' + jobId);
Test_Result__c job;
try {
job = [SELECT Id, JobId__c, TestClassIds__c FROM Test_Result__c WHERE JobId__c = :jobId LIMIT 1];
System.debug('Fetched Test_Result__c record: ' + job);
} catch (Exception e) {
System.debug('Exception querying Test_Result__c: ' + e.getMessage());
return new Map<String, Object>{
'status' => 'Error',
'message' => 'Error querying job record: ' + e.getMessage()
};
}
List<String> testClassIds = new List<String>();
try {
String decodedTestClassIds = decodeHtml(job.TestClassIds__c);
System.debug('Decoded test class IDs: ' + decodedTestClassIds);
List<Object> parsedIds = (List<Object>)JSON.deserializeUntyped(decodedTestClassIds);
for (Object obj : parsedIds) {
testClassIds.add(String.valueOf(obj));
}
System.debug('Deserialized test class IDs: ' + testClassIds);
} catch (Exception e) {
System.debug('Exception deserializing TestClassIds__c: ' + e.getMessage());
return new Map<String, Object>{
'status' => 'Error',
'message' => 'Error deserializing test class IDs: ' + e.getMessage()
};
}
// Check if all tests have completed
List<ApexTestQueueItem> queueItems;
try {
queueItems = [SELECT Id, Status FROM ApexTestQueueItem WHERE ApexClassId IN :testClassIds];
System.debug('Fetched ApexTestQueueItem records: ' + queueItems);
} catch (Exception e) {
System.debug('Exception querying ApexTestQueueItem: ' + e.getMessage());
return new Map<String, Object>{
'status' => 'Error',
'message' => 'Error querying test queue items: ' + e.getMessage()
};
}
Boolean allCompleted = true;
for (ApexTestQueueItem item : queueItems) {
System.debug('TestQueueItem status: ' + item.Status);
if (item.Status != 'Completed') {
allCompleted = false;
break;
}
}
if (!allCompleted) {
System.debug('Tests are still running for jobId: ' + jobId);
return new Map<String, Object>{
'status' => 'InProgress',
'message' => 'Tests are still running'
};
}
System.debug('All tests completed for jobId: ' + jobId + '. Fetching coverage results.');
// Fetch code coverage results using Tooling API
List<CoverageData> coverageList;
try {
coverageList = fetchCodeCoverage(testClassIds);
System.debug('Fetched coverage results: ' + coverageList);
} catch (Exception e) {
System.debug('Exception fetching code coverage: ' + e.getMessage());
return new Map<String, Object>{
'status' => 'Error',
'message' => 'Error fetching code coverage: ' + e.getMessage()
};
}
// Calculate the overall coverage percentage
Double totalCoverage = 0;
if (!coverageList.isEmpty()) {
Double sumOfCoveragePercentages = 0;
for (CoverageData data : coverageList) {
sumOfCoveragePercentages += data.coveragePercentage;
}
totalCoverage = sumOfCoveragePercentages / coverageList.size();
}
System.debug('Calculated total coverage: ' + totalCoverage);
// Update the Test_Result__c record with the coverage percentage
try {
job.Coverage_Percent__c = totalCoverage;
update job;
System.debug('Updated Test_Result__c with Coverage_Percent__c: ' + job);
} catch (Exception e) {
System.debug('Exception updating Coverage_Percent__c: ' + e.getMessage());
return new Map<String, Object>{
'status' => 'Error',
'message' => 'Error updating job record: ' + e.getMessage()
};
}
Map<String, Object> result = new Map<String, Object>();
result.put('status', 'Completed');
result.put('coverageData', coverageList);
result.put('coveragePercentage', totalCoverage);
return result;
}
@AuraEnabled
global static void cleanUpJob(String jobId) {
// Clean up the job record after completion
delete [SELECT Id FROM Test_Result__c WHERE JobId__c = :jobId];
}
private static List<CoverageData> fetchCodeCoverage(List<String> testClassIds) {
String query = 'SELECT ApexClassOrTrigger.Name, NumLinesCovered, NumLinesUncovered ' +
'FROM ApexCodeCoverageAggregate ' +
'WHERE ApexClassOrTriggerId IN ('' + String.join(testClassIds, '','') + '') ' +
'ORDER BY ApexClassOrTrigger.Name ASC';
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:SalesforceAPI/services/data/v53.0/tooling/query/?q=' + EncodingUtil.urlEncode(query, 'UTF-8'));
req.setMethod('GET');
req.setHeader('Content-Type', 'application/json');
req.setHeader('Authorization', 'Bearer {!$Credential.OAuthToken}');
System.debug('Tooling API request endpoint: ' + req.getEndpoint());
System.debug('Tooling API request query: ' + query);
Http http = new Http();
HttpResponse res;
try {
res = http.send(req);
System.debug('Tooling API callout response status: ' + res.getStatus());
System.debug('Tooling API callout response body: ' + res.getBody());
} catch (Exception e) {
System.debug('Tooling API callout exception: ' + e.getMessage());
throw new CalloutException('Failed to fetch code coverage data: ' + e.getMessage());
}
List<CoverageData> coverageList = new List<CoverageData>();
if (res.getStatusCode() == 200) {
try {
testCoverageWrapper tcw = (testCoverageWrapper) JSON.deserialize(res.getBody(), testCoverageWrapper.class);
System.debug('Deserialized testCoverageWrapper: ' + tcw);
for (testCoverageWrapper.records record : tcw.records) {
System.debug('Processing record: ' + record);
String name = record.ApexClassOrTrigger.Name;
Integer numLinesCovered = record.NumLinesCovered;
Integer numLinesUncovered = record.NumLinesUncovered;
Integer totalLines = numLinesCovered + numLinesUncovered;
Double coveragePercent = (totalLines > 0) ? ((Double)numLinesCovered / totalLines) * 100 : 0;
System.debug('Coverage Data - Name: ' + name + ', Covered: ' + numLinesCovered + ', Uncovered: ' + numLinesUncovered + ', Percent: ' + coveragePercent);
coverageList.add(new CoverageData(name, numLinesCovered, numLinesUncovered, coveragePercent));
}
} catch (Exception e) {
System.debug('Exception deserializing JSON response: ' + e.getMessage());
System.debug('Response body: ' + res.getBody());
throw new CalloutException('Failed to deserialize code coverage data: ' + e.getMessage());
}
} else {
System.debug('Error from Tooling API: ' + res.getBody());
throw new CalloutException('Tooling API call failed with status: ' + res.getStatus());
}
return coverageList;
}
private static String decodeHtml(String input) {
input = input.replace('"', '"');
input = input.replace('&', '&');
input = input.replace('<', '<');
input = input.replace('>', '>');
input.replace(''', ''');
return input;
}
}
Jonah Wooten is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.