Automatically Generate Apex Test Class For SuPICE

By March 20, 2017 Blog No Comments
automatically-generate-apex-test-class-for-supice-1

Generate Apex code

SuPICE is a great tool to create Lightning Components without any coding. To achieve this, you will need to set fields to support all sorts of data types.

For example, in a text search feature, some of the alignment with Data type * function *value, will not always work property.

  • If the numeric value is null, inequality sign will not function.
  • If the check box value is null, it will search as false setting.
  • Include/exclude value can only be used in Select list.
  • You cannot search Long Text Area and Rich Text Area.

You will need to run a test for all patterns; there will be hundreds and thousands of similar methods, which can be a hassle.

I would like to introduce a way to generate Apex Code test methods from node.js and to manage similar tests.

gulp Task

Here is the code to generate and deploy test methods from a Salesforce field definition as a gulp task.

Define fields

Fields will be used for “Define fields” and for “generate test”, so it is better to define them as different files for multiple use.

fields.js

module.exports = function () {

  const types = [

    ‘Checkbox’,

    ‘Currency’,

    ‘Date’,

    ‘DateTime’,

    ‘Email’,

    ‘Number’,

    ‘Percent’,

    ‘Phone’,

    ‘Picklist’,

    ‘MultiselectPicklist’,

    ‘Text’,

    ‘TextArea’,

    ‘LongTextArea’,

    ‘Url’,

    ‘Html’

  ];

  var fields = types.map(function (type) {

    var field = {};

    field.type = type;

    field.label = type;

    field.fullName = type + ’01__c';

    if (type === ‘Text’) {

      field.length = 255;

    }

    if ([‘Currency’, ‘Number’, ‘Percent’].indexOf(type) >= 0) {

      field.precision = 18;

      field.scale = 0;

    }

    if ([‘MultiselectPicklist’, ‘Html’, ‘LongTextArea’].indexOf(type) >= 0) {

      field.visibleLines = 10;

    }

    if (type === ‘Checkbox’) {

      field.defaultValue = ‘false';

    }

    if (type === ‘LongTextArea’ || type === ‘Html’) {

      field.length = 32000;

    }

    if (type === ‘MultiselectPicklist’ || type === ‘Picklist’) {

      field.picklist = {

        picklistValues: [

          { fullName: ‘1’},

          { fullName: ‘2’},

          { fullName: ‘3’},

          { fullName: ‘4’},

          { fullName: ‘5’}

        ]

      };

    }

    return field;

  });

  return fields;

};

Meta data

Generating and Deploy for Objects, Field authority (for Admins), and classes are as follows:

Method will be generated by cartesian-product, returns intersection from multiple rows

gulpfile.js

const fs = require(‘fs’);

const gulp = require(‘gulp’);

const zip = require(‘gulp-zip’);

const file = require(‘gulp-file’);

const deploy = require(‘gulp-jsforce-deploy’);

const through = require(‘through2′);

const metadata = require(‘salesforce-metadata-xml-builder’);

const product = require(‘cartesian-product’);

const meta = require(‘jsforce-metadata-tools’);

const FIELD_LIST = require(‘./fields.js’)();

const API_VERSION = ‘36.0’;

const CLASS_NAME = ‘TestToSOQL';

const OBJECT_NAME = ‘TestObject__c';

const SF_USERNAME = process.env.SF_USERNAME;

const SF_PASSWORD = process.env.SF_PASSWORD;

const FIELDS = FIELD_LIST.map((f) => f.fullName);

const OPERATORS = [

  {name: ‘eq’  , exp: ‘=’},

  {name: ‘lt’  , exp: ‘<‘},

  {name: ‘gt’  , exp: ‘>’},

  {name: ‘le’  , exp: ‘<=’},

  {name: ‘ge’  , exp: ‘>=’},

  {name: ‘like’, exp: ‘LIKE’},

  {name: ‘inc’ , exp: ‘INCLUDES’},

  {name: ‘exc’ , exp: ‘EXCLUDES’}

];

const VALUES = [

  {name: ‘number’  , exp: ‘1’ },

  {name: ‘string’  , exp: ‘\’1\” },

  {name: ‘null’    , exp: ‘null’ },

  {name: ‘list’    , exp: ‘new String[]{\’1\’}’ },

  {name: ‘bool’    , exp: ‘true’ },

  {name: ‘date’    , exp: ‘Date.today()’ },

  {name: ‘datetime’, exp: ‘DateTime.now()’ }

];

 

function makeQueryTestClass(name) {

  const methods = product([

    FIELDS, OPERATORS, VALUES

  ]).map((args) => {

    const field = args[0];

    const op = args[1];

    const val = args[2];

    return ‘

    @isTest

    static void test_${field.replace(‘__c’, ”)}_${op.name}_${val.name}() {

      Object v = ${val.exp};

      SObject[] records = Database.query(‘SELECT Id FROM ${OBJECT_NAME} WHERE ${field} ${op.exp} :v ‘);

    }';

  });

  return ‘

@isTest

class ${name} {

  ${methods.join(‘\n’)}

}

‘;

}

function makeObject() {

  const label = OBJECT_NAME.replace(‘__c’, ”);

  return {

    label: label,

    pluralLabel: label,

    fields: FIELD_LIST,

    nameField: {

      type: ‘AutoNumber’,

      label: ‘Number’

    },

    deploymentStatus: ‘Deployed’,

    sharingModel: ‘ReadWrite’

  }

}

 

function makeProfile() {

  return {

    name: ‘Admin’,

    objectPermissions: [{

      allowRead: true,

      allowCreate: true,

      allowEdit: true,

      allowDelete: true,

      object: OBJECT_NAME

    }],

    fieldPermissions: FIELDS.map((f) => {

      return {field: ‘${OBJECT_NAME}.${f}’, readable: true, editable: true}

    })

  };

}

 

gulp.task(‘deploy’, () => {

  const classbody = makeQueryTestClass(CLASS_NAME);

  const classmeta = metadata.ApexClass({ apiVersion: API_VERSION, status: ‘Active’ });

  const objectxml = metadata.CustomObject(makeObject());

  const profilexml = metadata.Profile(makeProfile());

  const packagexml = metadata.Package({ version: API_VERSION, types: [

    { name: ‘ApexClass’, members: [‘*’] },

    { name: ‘CustomObject’, members: [‘*’] }

  ]});

 

  through.obj()

    .pipe(file(‘src/classes/${CLASS_NAME}.cls’, classbody, {src: true}))

    .pipe(file(‘src/classes/${CLASS_NAME}.cls-meta.xml’, classmeta))

    .pipe(file(‘src/objects/${OBJECT_NAME}.object’, objectxml))

    .pipe(file(‘src/profiles/Admin.profile’, profilexml))

    .pipe(file(‘src/package.xml’, packagexml))

    // .pipe(gulp.dest(‘./tmp’))

    .pipe(zip(‘pkg.zip’))

    .pipe(deploy({

      username: SF_USERNAME,

      password: SF_PASSWORD,

      rollbackOnError: true

    }));

 

});

 

If you run:

$ gulp deploy       

from the command line, the following will be generated and deployed:

  • Object for tests
  • Give authority to admin profile field
  • Test Class

This process actually consists of calling SuPICE package’s query and assertions instead of directly running SOQL.

This code will generate 840 test methods by the following equation: Fields (15) x Functions (8) x condition value (7).

Run Test

This is a task to run a test for the deployed Apex Test class and output the result as TSV file.

gulp.task(‘test’, () => {

  const packagexml = metadata.Package({ version: API_VERSION, types: []});

  const testdeploy = through.obj((file, enc, callback) => {

    meta.deployFromZipStream(file.contents, {

      username: SF_USERNAME,

      password: SF_PASSWORD,

      testLevel: ‘RunSpecifiedTests’,

      runTests: [CLASS_NAME],

      pollTimeout: 180e3,

      pollInterval: 10e3

    })

      .then(function(res) {

        const testResult = res.details.runTestResult;

        function splitMethodName(methodName) {

          return methodName.split(‘_’).slice(1);

        }

        function makeTSVLine(r, resultText) {

          const method = splitMethodName(s.methodName);

          const arr = [r.message, method[0], method[1], method[2]];

          if (r.message) { arr.push(r.message); }

          return arr.join(‘\t’);

        }

        const successLines = testResult.successes.map((r) => makeTSVLine(r, ‘success’));

        const failureLines = testResult.successes.map((r) => makeTSVLine(r, ‘fail’));

        const lines = successLines.concat(failureLines);

        fs.writeFileSync(‘testResult.tsv’, lines.join(‘\n’), ‘utf8′);

        callback(null, file);

      })

      .catch(function(err) {

        callback(err);

      });

  });

  return through.obj()

    .pipe(file(‘src/package.xml’, packagexml, {src: true}))

    .pipe(zip(‘pkg.zip’))

    .pipe(testdeploy);

});

$ gulp test                        

Run this to start the test and output the result as TSV file.

Challenges

So, I have explained on how to generate Apex Test methods. However, there are challenges with this approach.

Generating tests from each parameter settings will produce inexplicable test patterns, which increases the number of methods compared to coding test methods using Apex. Also, to decide on the assertion for each pattern will make you write branches during the generation process, which will result in re-implementing tests. This will generate unnecessary code with Apex and will still result in a large number of methods with stressful maintenance.

Here is a solution to solve this issue:

If each parameter that loops when generated is given a unique name, you can place them in a hierarchy map. Field name, function, criteria value will each have a unique name and use that to generate method name which will look like this:

{

  Text01__c: {

    eq: {

      string: {},

      number: {},

      //

      //

      //

}

It would be simpler if you could code test generation, exclusion flag, or assertion type in this category. Coding in raw object will end up with large amount of code. Instead, use template strings, it has less coding than JSON and you can define keys.

Below is how it will turn out:

const objexp = require(‘objexp’);

const get = require(‘lodash.get’);

 

const NUMERIC_FIELDS = [‘Currency01__c’, ‘Number01__c’, ‘Percent01__c’];

const INEQUALITY_OPERATORS = [‘lt’, ‘gt’, ‘le’, ‘ge’];

const ALL_VALUES = VALUES.map((v) => v.name);

const data = {

    INEQUALITY_OPERATORS,

    NUMERIC_FIELDS,

    ALL_VALUES

};

 

const IGNORE_CASES = objexp(‘

    $NUMERIC_FIELDS like                    $ALL_VALUES

    Picklist01__c   $INEQUALITY_OPERATORS   $ALL_VALUES

‘, {data});

//=> {

//     Currency01__c: {

//       like: {

//         number: {},

//         string: {},

//         null: {},

//         list: {},

//         bool: {},

//         date: {},

//         datetime: {}

//       }

//     }

//     Picklist01__c: {

//       lt: {

//         number: {},

//         string: {},

//         null: {},

//         list: {},

//         bool: {},

//         date: {},

//         datetime: {}

//       },

//       gt: {…},

//       le: {…},

//       ge: {…}

//     }

//   }

 

product([

    FIELDS, OPERATORS, VALUES

  ])

  .filter((args) => !get(IGNORE_CASES, args))// IGNORE_CASES

  .forEach((args) => {

    // generate apex methods

  });

Summary

Although there are some challenges in generating Apex Test Classes, there are benefits of having some process for testing. This leads to uniform management, easier to edit existing tests, reduce the volume of source code, etc. Keep in mind that this just one of the approaches to implement test methods.

*Feel free to check out the source code used in this blog from here