Building applications in ServiceNow is one of those things that sits at the top of my fun things to do – usually. When it’s not so fun is when the code I write doesn’t do what I want it to do or worse–it breaks.
I find that writing code so I can test it outside of itself helps with managing the overall build of the application. I’m referring to unit testing. The way I like to look at unit testing is that I write my ServiceNow code in such a way that I can test it from Scripts – Background to see the results without having to run the entire code base. What the heck do I mean? Here’s an example.
Let’s say that when I enter an email address into a form and save the form I want to ensure that email address is not already associated with an existing User record. If that email is already in use by an existing user record, then I want to prevent the submitted record from being saved and I want to warn the submitter that the email is already in use by an existing user account.
The first thing I think about is to use a Before Business Rule on the table where I’m trying to create the record. This before business rule will:
- Get that email address.
- Query the sys_user table to see if a record exists with that email address.
- Stop the save process.
- Produce an onscreen message for the submitter.
Now – I can write this code directly in the business rule, like this.
(function executeRule(current, previous /*null when async*/) {
/*
What's going on here mate?
1. I cannot allow an email address for a new team member to match an email of an existing sys_user record's email address.
2. If the email on a form that is trying to create a new team member matches an existing sys_user email, then:
a. Abort the insert.
b. Warn the submitter.
*/
// Put the email into a variable for easier code reading
var email = current.getValue('email');
// Now query the sys_user table to find 1 sys_user record that has that email
var sysUser = new GlideRecord('sys_user');
// now, I like to put my gliderecord queries in an encodedQuery that I can build from a filtered list view
var encodedQuery = 'email=' + email;
sysUser.addEncodedQuery(encodedQuery);
// now limit results to just 1 - as long as I find 1 sys_user record I want to abort the insert of a new team member record
sysUser.setLimit(1);
sysUser.query();
if (sysUser.next()){
// I found 1 record, so bug out, man.
current.setAbortAction(true);
gs.addErrorMessage("Sorry folks, but this email [" + email + "] is already being used by Sys User record.");
}
})(current, previous);
But keep in mind, I want to be able to unit test this functionality from Scripts – Background. If the logic to search for an existing sys_user record with a matching email stays in the business rule, then I must open a new record form whenever I want to test this. I could copy this code in its entirety into scripts – background, but that introduces the risk of copy and pasting errors.
Instead – let’s refactor this code and move the duplicate email checking into a Script Include. It requires 2 steps:
- Create the script include (see this video).
- Refactor the business rule code to use the script include.
- Unit test in Scripts – Background
Let’s do this.
First, if we don’t already have a script include we need to create one – watch this video to create a script include.
Next, we want to move the logic out of the business rule and put it into the script include. This is the code currently in the business rule that is checking for an existing sys_user record that has the email. We’ll move this into the script include:
// Now query the sys_user table to find 1 sys_user record that has that email
var sysUser = new GlideRecord('sys_user');
// now, I like to put my gliderecord queries in an encodedQuery that I can build from a filtered list view
var encodedQuery = 'email=' + email;
sysUser.addEncodedQuery(encodedQuery);
// now limit results to just 1 - as long as I find 1 sys_user record I want to abort the insert of a new team member record
sysUser.setLimit(1);
sysUser.query();
if (sysUser.next()){
// I found 1 record, so bug out, man.
current.setAbortAction(true);
gs.addErrorMessage("Sorry folks, but this email [" + email + "] is already being used by Sys User record.");
}
To do this we open the script include and add a new function. Here’s a new ‘findSysUserWithMatchingData’ function we added to our existing script include:
var TeamMemberUtil = Class.create();
TeamMemberUtil.prototype = {
initialize: function() {
},
findSysUserWithMatchingData: function(encodedQuery){
// Now query the sys_user table to find 1 sys_user record that has that email
var sysUser = new GlideRecord('sys_user');
// now, I like to put my gliderecord queries in an encodedQuery that I can build from a filtered list view
// we create a flexible script include by passing in an encodedQuery from other code (business rule, scheduled script execution, etc)
sysUser.addEncodedQuery(encodedQuery);
// now limit results to just 1 - as long as I find 1 sys_user record I want to abort the insert of a new team member record
sysUser.setLimit(1);
sysUser.query();
if (sysUser.next()){
// I found 1 record, so bug out, man.
return true; // ensure we return true if we found a recurd
}
return false; // return false if we made it here - indicating no matching record was found
},
type: 'TeamMemberUtil'
};
Now that we moved our logic to the script include, we need to adjust our business rule logic to use the script include’s code. Here’s the updated business rule code:
(function executeRule(current, previous /*null when async*/) {
/*
What's going on here mate?
1. I cannot allow an email address for a new team member to match an email of an existing sys_user record's email address.
2. If the email on a form that is trying to create a new team member matches an existing sys_user email, then:
a. Abort the insert.
b. Warn the submitter.
*/
// Put the email into a variable for easier code reading
var email = current.getValue('email');
// Now query the sys_user table to find 1 sys_user record that has that email
var teamMemberUtil = new TeamMemberUtil();
var encodedQuery = 'email=' + email;
var matchingSysUserFound = teamMemberUtil.findSysUserWithMatchingData(encodedQuery);
if (matchingSysUserFound){
// I found 1 record, so bug out, man.
current.setAbortAction(true);
gs.addErrorMessage("Sorry folks, but this email [" + email + "] is already being used by Sys User record.");
}
})(current, previous);
Next, we want to unit test this in scripts – background. Here’s how to do it:
- Copy the code from the business rule into scripts background.
- Add a variable for the email
- Run the code to see the results
Here’s the code to copy into scripts – background (Note: comment out the line that aborts. This is because you don’t have access to the ‘current’ object when running this code in scripts – background).
// Put the email into a variable for easier code reading
var email = 'peter.partner@example.com'; // use an existing sys_user record's email
// Now query the sys_user table to find 1 sys_user record that has that email
var teamMemberUtil = new TeamMemberUtil();
var encodedQuery = 'email=' + email;
var matchingSysUserFound = teamMemberUtil.findSysUserWithMatchingData(encodedQuery);
if (matchingSysUserFound){
// I found 1 record, so bug out, man.
//current.setAbortAction(true);
gs.addErrorMessage("Sorry folks, but this email [" + email + "] is already being used by Sys User record.");
}
When you run this code in scripts – background you should see your error message. If any of the code is broken, you’ll see error messages. This process allows you to unit test your script include’s logic using scripts – background.