treycarroll
Giga Guru

I know that we're all excited to start using the Automated Test Framework, but there is still a place for unit testing in ServiceNow development.   Specifically, verifying the correctness of functions in Script Includes is worthwhile.   (And a must for any team following a Test-Driven Development methodology such as XP.)

Unit Testing involves verifying the correctness of the implementation of a function by crafting inputs and validating outputs.     Take a simple example:

var Calculator = Class.create();

Calculator.prototype = {

      initialize: function() {

      },

      sum: function(addend1, addend2){

              return addend1 + addend2;

      },

      type: 'Calculator'

};

How would we verify that the implementation of the sum function is correct?     Here is a simple unit test for the sum function:

Fix script to verify that sum function correctly adds 2 positive numbers:

// Arrange

var calc = new Calculator();

var parameter1 = 2;

var parameter2 = 2;

var expectedResult = 4;

//Act

var actualResult = calc.sum(parameter1, parameter2);

//Assert

gs.print("Two + Two should equal Four.")

if(actualResult == expectedResult){

    gs.print("Pass");

} else{

    gs.print("Fail");

}

We could add more tests for further cases e.g. adding two negative numbers, one positive and one negative number, adding with a zero parameter, etc.   Just in case you think that this example is so simple that nothing could go wrong, consider what would happen if we did the following:

var actualResult = calc.sum("2", "2");

Failing tests cause us to think more deeply, and often to refactor our implementation and to reconsider our design "contract".     We might add code to check the types of our parameters and specify preconditions regarding expectations.   We might specify that the function throw a particular kind of exception when those conditions are not met.   In this case, it makes sense to add a precondition that both parameters must be of type number.

var Calculator = Class.create();

Calculator.prototype = {

      initialize: function() {

      },

      sum: function(addend1, addend2){

              var addendOneType = typeof addend1;

              var addendTwoType = typeof addend2;

                if( addendOneType == 'number'   && addendTwoType == 'number' ) {

                        return addend1 + addend2;

              } else {

                      throw "Exception: both parameters of sum function must be numbers.   Param1 type:" + addendOneType + " Param2 type: " + addendTwoType;

                }

      },

      type: 'Calculator'

};

Now we add an additional test to ensure that we throw an error for non-numeric parameters:

// Arrange

var calc = new Calculator();

var parameter1 = "2";

var parameter2 = 2;

var expectedResult = "Exception";

//Act

try{

    var actualResult = calc.sum(parameter1, parameter2);

} catch(e){

    actualResult = "Exception:"+ e.message;

}

//Assert

gs.print("String Two + number Two should throw an Exception.")

if(actualResult.indexOf(expectedResult) > -1){ //The string "Exception" was found in the actual result.

      gs.print("Pass");

} else {

      gs.print("Fail");

}

But realistically, most of our Script Includes are going to perform calculations and operations based on GlideRecord inputs.   How could we unit test such functions?

In order to facilitate Unit Testing we use a design pattern called Dependency Injection (DI) or Inversion of Control (IOC).   This means that where our function needs an object as an input for an operation, that object should be passed in as a parameter instead of instantiating it directly in the function.   Let's take a look:

This implementation does not use DI/IOC, does not facilitate testability:

var u_Example = Class.create();

  u_Example.prototype = {

  initialize: function() {

  },

  sameLastName:function(userId1, userId2){

        var grUser1 = new GlideRecord("sys_user");

        var grUser2 = new GlideRecord("sys_user");

        if (grUser1.get(userId1) && grUser2.get(userId2) ) {

                return grUser1.last_name == grUser2.last_name;

        }

  },

type: 'u_Example'

};

This implementation of sameLastName passes in the GlideRecords, following the DI/IOC pattern:

sameLastName:function(grUser1, grUser2){

      return grUser1.last_name == grUser2.last_name;

},

Of course we could "new up" a couple of actual GlideRecords pointing to a real user record for our test, but using mocks is more convenient (and makes the test lightning fast since it doesn't involve actually going to the database).

// Arrange

var obj = new u_Example();

var mockUser1 = {

    last_name:"Smith"

};

var mockUser2 = {

    last_name:"Patel"

};

var expectedResult = false;

//Act

var actualResult = obj.sameLastName(mockUser1, mockUser2);

//Assert

gs.print("sameLastName() should return false when passed Smith and Patel.");

if(actualResult == expectedResult){

    gs.print("Pass");

} else{

    gs.print("Fail");

}

But, in practice, Script Includes often involve accessing nested properties from GlideRecords for tables with reference fields.   Can mocks address such complexity?   Absolutely.   Consider the following mock that starts with a sysapproval_approver record:

//This is a real-world mock that I use to test a Script Include that supports a Business Rule on the sysapproval_approver table

var mock1 = {

  approver:'e5aadc386f319500b8d923fc5d3ee406',

  sysapproval:{

        cat_item:{

            u_approval_escalation:false,

            u_allow_requestor_approval: true

        },

      sys_class_name:'sc_req_item',

      u_requested_for:'e5aadc386f319500b8d923fc5d3ee406',

      toString: function(){return 'c9ba4b4f1385b600febc79d96144b0de';}

  },

};

You may notice that these can get somewhat complex.     As the object's property tree grows, it becomes more difficult to do a quick visual inspection to confirm correctness.   After spending too much time trouble-shooting small mistakes on my mocks, I decided that I needed to make mock-building faster and less error-prone.   That was the impetus behind the creation of this helper class:

var GMF_MockBuilder = Class.create();

GMF_MockBuilder.prototype = {

initialize: function() {

},

// @param table is the name of the table

// @param aryFields is an array of objects where the properties are:

// For a simple field type

//       1) a fieldName key   (string)

//       2) a fieldValue key (string)

// For reference-type fields

//       1) a fieldName key (string)

//       2) a ref key (boolean)

//       3) a table key (string) - the referenced table name

//       4) a fieldsArray key - (array) - the fields to add from the referenced table.   Works recursively for nested tables.

make: function(table, aryFields ){

        var mock = {};

        try{

        //loop over all of the field in the field array param

        for (var k=0; k < aryFields.length; k++) {

        var fieldName = aryFields[k].fieldName;

        var fieldValue = aryFields[k].fieldValue;

        // look for a column entry for that field name in the sys_dictionary table

        var gr = new GlideRecord("sys_dictionary");

        gr.addQuery("name", table);

        gr.addQuery("element", fieldName);

        gr.query();

        if ( gr.next() ) {                  

                  var addReference = aryFields[k].addReference;

                  if( addReference == true) {

                            if(gr['internal_type'] == 'reference'){

                                      mock[fieldName] = this.make(aryFields[k].table, aryFields[k].fieldsArray);

                            } else {

                                      throw "Error creating mock object.   Marked non-reference field as reference type: " + fieldName ;

                            }

                  } else {

                            mock[fieldName] = fieldValue;

                  }

                  if(gr.display == true){

                            mock.toString = function(){ return fieldValue + '';};

                  }

        } else {

                  throw "Error creating mock object.   Field " + fieldName + " does not exist.";

        }

        }

  } catch(e) {

        if(gs.hasRole('admin')){

                  gs.addErrorMessage(e.message);

        }

        gs.logError(e.message, 'SI: GMF_MockBuilder');

}

return mock;

},

//@param table is the name of the table

//@param sys_id is the id for the record we want to start pulling values from

//@param fields array is an array of objects where the keys are

// For non-reference-type fields:

//       1) a fieldName key (string)

// For reference-type fields:

//       1) a fieldName key (string)

//       2) a ref key (boolean)   - Always true when present. Marks the field as a reference which should be followed.

//       3) a fieldsArray key (array) - A nested fieldsArray for the fields we want to pick up off of the referenced record.

makeFromRecord: function( table, sys_id, fieldsArray ){

        gs.print('In makeFromRecord table:' + table + 'sys_id:'+sys_id + ' fieldsArray:'+fieldsArray.toString());

        var mock =   {};

        //Look up the record in question

        var grRec = new GlideRecord(table);

        if ( grRec.get(sys_id) ) {

                  var actualTableName = grRec.sys_class_name || table;

       

                  //If this table has extending tables, go down to the real table and re-fetch the record.   (necessary to get child-specific fields)

                  if(new TableUtils(table).hasExtensions()){

                            grRec = new GlideRecord(actualTableName);

                            grRec.get(sys_id);

                  }

                  //Loop over the fieldsArray

                  for ( var i=0; i< fieldsArray.length; i++ ) {

                            var fieldName = fieldsArray[i].fieldName;

                            var followReference = fieldsArray[i].ref;

                            //Reference type field.   Recursively add child objects

                            if( followReference == true ){

                                      var grDict = new GlideRecord("sys_dictionary");

                                      grDict.addQuery("name", actualTableName);

                                      grDict.addQuery("element", fieldName);

                                      grDict.addQuery("internal_type", 'reference');

                                      grDict.query();

                                      if (grDict.next()) {

                                                var referencedTable = grDict.reference;

                                                var referencedRecId = grRec[fieldName].sys_id;

                                                mock[fieldName] = this.makeFromRecord(referencedTable, referencedRecId, fieldsArray[i].fieldsArray);                                              

                                      } else {

                                                throw "Could not find dictionary entry for reference field:" + fieldName + " for table " + actualTableName;

                                      }

                              } else { // Non-reference field.

                                      if( grRec.getValue(fieldName+'')){

                                                mock[fieldName] = grRec.getValue(fieldName+'');

                                      } else {

                                                throw "Attempted to add bad field name. Table: " + table + '|' + sys_id + " Field:" + fieldName;

                                      }

                              }

                  }//for

        } else {

                  throw "Could not locate record in table:" + table + " with id:" + sys_id;

        }

        return mock;

        },

        type: 'GMF_MockBuilder'

};

The make() function is for building your own record field by field (including inserting field values manually).   It has the advantage of checking your properties against the actual dictionary record to make sure that it exists.   (Those types of errors can be enormous time wasters.)

Here is an example of the use of the make() function:

var fieldsArray = [];

fieldsArray.push({

        fieldName:'first_name',

        fieldValue:'Fred'

});

fieldsArray.push({

        fieldName:'last_name',

        fieldValue:'Smith'

});

fieldsArray.push({

        fieldName:'manager',

        addReference: true,

        table:'sys_user',

        fieldsArray:[{

                  fieldName:'first_name',

                  fieldValue:'Jeff'

        }]

});

fieldsArray.push({

        fieldName:'location',

        addReference: true,

        table:'cmn_location',

        fieldsArray:[{

                  fieldName:'name',

                  fieldValue:'Philadelphia - 711'

        }]

});

var mock = new GMF_MockBuilder().make('sys_user', fieldsArray);

By contrast, the makeFromRecord() function pulls the values from real records found in the database.   You just supply the field names and whether or not the field is a reference that will need to be followed to further fields for the mock.

Here is an example of the use of the makeFromRecord() function:

var fieldsArray2 = [];

fieldsArray2.push({fieldName:'approver'});

// The object has nested fieldArray objects when following reference fields

fieldsArray2.push({ fieldName:'sysapproval', ref:true, fieldsArray:[

    {fieldName:'sys_class_name'},

    {fieldName:'u_requested_for'},

    {fieldName:'cat_item', ref:true, fieldsArray:[

              {fieldName:'u_allow_requestor_approval'},

              {fieldName:'u_approval_escalation'}

    ]}

]});

//"Feed" the function the table name and id of a record, then use the fieldsArray parameter to tell it which fields to add to the mock, including fields on referenced fields.

var mock2 = new GMF_MockBuilder().makeFromRecord('sysapproval_approver', 'd7edc38713c57a00c90132228144b0cc', fieldsArray2);

All you have to do is point makeFromRecord() at a particular record and it will build out your object pulling in the values even from nested references.   That's pretty cool.

I sometimes print these out in a fix script and then copy them into my actual test script.     If you're using makeFromRecord() in a sub-prod instance (where a clone can wipe out your records), or if your team-mates might monkey with your test fixtures you should do this:

gs.print( new global.JSON().encode(mock) );

The copied and pasted result would look something like this:

var persistentMock = {"approver":"e5aadc386f319500b8d923fc5d3ee406","sysapproval":{"cat_item":{"u_allow_requestor_approval":"1","u_approval_escalation":"0"},"sys_class_name":"sc_req_item","u_requested_for":"faba90786f319500b8d923fc5d3ee453"}};

Notice that JSON.encode uses zeros and ones instead of trues and falses.   As long as your code treats the values in a boolean expression they will work fine.

4 Comments