Sunday, 1 September 2019

Parallel Apex Unit Tests and Salesforce CLI Plugins

Parallel Apex Unit Tests and Salesforce CLI Plugins

 

Introduction

In the Salesforce Winter 20 release notes was something I’ve been looking forward to for a few years - the ability to turn off parallel Apex unit test execution. By default parallel unit test are enabled (somewhat confusingly, by the fact that the Disable Parallel Apex Testing option is not checked):

 

and this typically results in a number of failures in my automated test runner package, as the tests can’t get exclusive access to write to the account others object tables.

This setting is one of the last items that I have to manually turn on when creating a scratch org, and that has been annoying me for a while. I could automate it via Selenium, but that feels like overkill, so I was very pleased to see the new Apex Settings metadata type. Among other features, this has the enableDisableParallelApexTesting field which allows me to check or uncheck the Disable Parallel Apex Testing checkbox, albeit via a metadata API deployment. The release notes also mentioned the to-be-deprecated OrgPreferenceSettings metadata object, and it turns out that this also has a mechanism for turning off parallel testing via the DisableParallelApexTesting setting, so there was no need for me to wait until Winter 20 as long as I could switch between the two mechanisms based on the API version the org is at.

Salesforce CLI Plugin

Regular readers of this blog will know that I’m a huge fan of the Salesforce Cli - I use it all the time and when I’m looking to do anything around developer tooling I always try to create a CLI plugin to host it. This was no different, although it presented a couple of challenges that I hadn’t taken on before:

  • Determining the API version that the org is at. If this is 46 or less (not sure how it would be possible to be on an earlier version of the API than Summer 19, but if Salesforce ever allow it I wanted to be covered) I need to deploy an OrgPreferences metadata type, 47 or higher I need to deploy an ApexSettings.
  • Metadata deployment from inside a plugin. 

After I scaffolded a new bbsfdx plugin and copied the commands/hello/org.ts example command to bb/test/parallel.ts, I set about solving them.

Determine the API Version

Whenever I’m doing anything new with a CLI plugin, my first port of call is the reference documentation for the Salesforce DX Core Library, and I wasn’t disappointed. I can find the API version for the org via the Connection.retrieveMaxAPIVersion function. Getting a Connection object is simple in a CLI plugin - just specify the requiresUsername property as true and a connection comes up with the rations via the org property supplied by the plugin. Getting the target API version is as simple as:

const conn = this.org.getConnection();
const apiVersion = await conn.retrieveMaxApiVersion();

So far so good.

Metadata Deployment

The simplest way to do this is to execute an existing Salesforce CLI deployment command, either force:source:deploy or force:mdapi:deploy, but I’m not a fan of this approach. Spawning a process to execute a Salesforce CLI command from within a CLI plugin seems clunky and inelegant, and it binds me to a command that I don’t control and which may be retired. I should be able replicate anything the standard commands do as I have access to the same underlying libraries.

This time the core library docs weren’t much help - there was a metadata property on the Connection object, but it didn’t have any detail, so time to look elsewhere. The Core library is built on Shinichi Tomita’s JSforce library, so I headed over to the docs for that. The API reference for the Metadata class was exactly what I was looking for, specifically the deploy method.

To deploy metadata, I need a zip file containing a manifest (package.xml) and the metadata files themselves in the directory structure mandated by the metadata API. In order to achieve this, I create a temporary directory and write the appropriate information depending on the API version (stored as a float value in the fltVersion variable):

let packageFile=join(targetDir, 'package.xml');
let packageContents='<?xml version="1.0" encoding="UTF-8"?>\n' + 
    '<Package xmlns="http://soap.sforce.com/2006/04/metadata">\n' +
    ' <types>\n' + 
    '   <name>Settings</name>\n';

let fltVersion=parseFloat(apiVersion);

if (fltVersion>46) {
  packageContents+='    <members>Apex</members>\n' + 
                   '  </types>\n' +
                   '  <version>47.0</version>\n' + 
                   '</Package>';
  let apex=join(settingsDir, 'Apex.settings');
  writeFileSync(apex, 
        '<?xml version="1.0" encoding="UTF-8"?>\n' + 
        '<ApexSettings xmlns="http://soap.sforce.com/2006/04/metadata">\n' +
        '  <enableDisableParallelApexTesting>' + attr + '</enableDisableParallelApexTesting>\n' +
        '</ApexSettings>\n'
          );
}
else {
  packageContents+='    <members>OrgPreference</members>\n' + 
                   '  </types>\n' +
                   '  <version>46.0</version>\n' + 
                   '</Package>';
  let orgPref=join(settingsDir, 'OrgPreference.settings');
  writeFileSync(orgPref, 
    '<?xml version="1.0" encoding="UTF-8"?>\n' + 
    '<OrgPreferenceSettings xmlns="http://soap.sforce.com/2006/04/metadata">\n' + 
    '  <preferences>\n' + 
    '     <settingName>DisableParallelApexTesting</settingName>\n' + 
    '     <settingValue>' + attr + '</settingValue>\n' + 
    '  </preferences>\n' + 
    '</OrgPreferenceSettings>\n'
  );
}

writeFileSync(packageFile, packageContents);

Now I have my directory structure, I need to zip it. Searching on npm for zip packages returns a lot of results, so how to choose? I bounced around sites like stack exchange to see what others were using and eventually settled on compressing for a couple of reasons. First, it supports other compression files than zip, and I might need that flexibility in the future, and second it has a really simple API and is already promisified. After installing it into my plugin node modules and importing it into the parallel.ts file, generating a zip file is a couple of lines:

const zipFile=join(tmpDir, 'pkg.zip');
await compressing.zip.compressDir(targetDir, zipFile);

Getting there. Back to the docs for the metadata deploy function, it wants a zip stream rather than a filename, so I create one of those and add the code to deploy the metadata;

let zipStream=createReadStream(zipFile);
let result=await conn.metadata.deploy(zipStream, {});

The deploy function returns information about the deployment job, so I then enter a loop to poll for the status until it is finished:

let done=false;

let deployResult:DeployResult;
while (!done) {
  deployResult=await conn.metadata.checkDeployStatus(result.id);
  done=deployResult.done;
  if (!done) {
    this.ux.log(deployResult.status + messages.getMessage('sleeping'));
    await new Promise(sleep => setTimeout(sleep, 5000));
  }
}

and there it is - a plugin to enable or disable parallel Apex unit testing in under a couple of hundred lines of Typescript (and obviously a ton of existing node modules that allow me to stand on the shoulders of giants). 

Where’s the Code?

The full code for the plugin can be found in the Github repository at : https://github.com/keirbowden/bbsfdx 

The plugin itself is published on npm at : https://www.npmjs.com/package/bbsfdx - it has been tested on MacOS.

To install the plugin into your version of sfdx, execute:

sfdx plugins:install bbsfdx

Related Posts

No comments:

Post a Comment