- Post History
- Subscribe to RSS Feed
- Mark as New
- Mark as Read
- Bookmark
- Subscribe
- Printer Friendly Page
- Report Inappropriate Content
03-07-2022 11:00 PM - edited 12-25-2022 04:59 AM
In my article Universal Pattern for Script Includes I introduced the Script Include CurrencyUtils which allows conversion between two currencies. At first glance this may seem simple, but in the end it turned into quite a lot of code that needs to be tested extensively. This article describes an approach to define and execute so-called developer tests quickly and easily, even in larger numbers. |
|
Challenge
The Script Include CurrencyUtils offers one public method with the following signature:
convert(strSourceCurrency, strTargetCurrency, strOriginalAmount, [strLocale])
Since in JavaScript, in contrast to compiled high-level languages like Java, a method can be called with any number of parameters and, in addition, a variable can have any data type at runtime. This results in a high number of possible combinations for invoking this method.
The following example invocations are therefore theoretically possible, but technically invalid:
convert();
convert('XYZ');
convert(111);
convert('USD', 'EUR');
convert('USD', 'XYZ', 'de_DE');
For this reason, the convert() method must be very robust, in order to catch as many error cases as possible and to react accordingly.
Identifying all invoking variants and translating them into appropriate tests, as well as executing these tests, can become very time-consuming. That is why a test approach is needed in which the effort remains as low as possible and thus to stay below the given budget limit.
Testing Approaches
JUnit is a popular unit testing framework for the Java programming language. With its help, it is possible to create new tests easily and thus support the Test Driven Development approach. In ServiceNow, we have the Automated Test Framework which offers the Run Server Side Script step for testing your Script Includes or other types of server-side code.
But the documentation is not clear enough about the fact that you have three different variants for leveraging that test step. For this reason, I will introduce them briefly.
My test case for the following approaches is inserting a record into the incident table. And I also configured a Data Policy to make the field "short_description" mandatory. This way I can test passing and forced failing scenarios easily.
(1) Basic Pattern with one Test per Step
With this variant, you have to return the function with "true" or "false". And you can add a log output message, but only one time:
(function(outputs, steps, stepResult, assertEqual) {
var grIncident = new GlideRecord('incident');
grIncident.setValue('short_description', 'My Test Description');
grIncident.insert();
if (grIncident.isActionAborted() || !grIncident.isValidRecord()) {
stepResult.setOutputMessage("Failed to insert incident record");
return false;
}
else {
stepResult.setOutputMessage("Successfully inserted incident record");
return true;
}
})(outputs, steps, stepResult, assertEqual);
passed test:
failed test:
(2) More JUnit-like pattern with one test per step
In Java a simple JUnit test could look like this:
package demo.tests;
import static org.junit.Assert.*;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
public class JUnitProgram {
public void test_JUnit() {
String strExpected = "This is the testcase in this class";
String strGiven = "This is the testcase in this class";
assertEquals(strExpected, strGiven);
}
}
The last code line with assertEquals represents the test. In the above example, it is evaluated whether the value of variable strExpected is equal to strGiven.
ServiceNow offers the same concept in a slightly modified form.
The passed parameter assertEqual represents a callback function which can be invoked with an object containing the following values:
- name: is taken for the result output of failed tests (see below screenshot)
- shouldbe: expected value
- value: given value
The modified version of the insert test looks as follows:
(function(outputs, steps, stepResult, assertEqual) {
var grIncident = new GlideRecord('incident');
grIncident.setValue('short_description', 'My Test Description');
grIncident.insert();
assertEqual({
name: "Insert incident record",
shouldbe: true,
value: !grIncident.isActionAborted() && grIncident.isValidRecord(),
});
})(outputs, steps, stepResult, assertEqual);
passed test:
The downside of this approach is the missing log output about what has been tested, in case the test has passed.
failed test:
In case of a failed test, you get displayed the configured output from the described object property name.
(3) Jasmine Framework with many Tests per Step
Regarding Jasmin, you will find very little information in the documentation, which is a pity. Jasmine is an open-source testing framework for JavaScript which has an easy-to-read syntax and is included in ServiceNow.
If you want to use Jasmin, the first thing you should do is uncommenting the last line in the example code loaded when creating the test step:
})(outputs, steps, stepResult, assertEqual);
jasmine.getEnv().execute();
Jasmin allows defining complete "test suites" by using describe() together with a kind of prefix text which will be displayed in the test output:
describe("Test Incident Insertion ", function() {
});
Within that describe block you can define as many test as you want by using it() in combination with a descriptive text which will be added to the prefix from describe() in the step output:
it("with no error message 1", function() {
});
The expected result is defined with expect() chained with a matcher function, which takes the expected value:
expect(true).toBe(true);
The following example combines everything into one test-suite consisting of several individual tests, representing both passing and failing checks:
(function(outputs, steps, stepResult, assertEqual) {
describe("Test Incident Insertion ", function() {
it("with no error message 1", function() {
var grIncident = new GlideRecord('incident');
grIncident.setValue('short_description', 'My Test Description');
grIncident.insert();
expect(true).toBe(!grIncident.isActionAborted() && grIncident.isValidRecord());
});
it("with no error message 2", function() {
var grIncident = new GlideRecord('incident');
grIncident.insert();
expect(true).toBe(!grIncident.isActionAborted() && grIncident.isValidRecord());
});
it("with error message", function() {
var grIncident = new GlideRecord('incident');
grIncident.insert();
expect(true).toBe(grIncident.isActionAborted() || !grIncident.isValidRecord());
});
});
})(outputs, steps, stepResult, assertEqual);
jasmine.getEnv().execute();
In the following test output, you can see that all 3 tests were performed with a descriptive message from each test:
Comparing all three approaches, only the last one makes sense to be followed up, since it is possible to define many tests in one test step and in case of a failed test the execution of the following tests is continued.
Solution
If you look at the source code of variant (3), you will notice that the definition of the tests is always very similar. Transferred to the creation of test cases for the CurrencyUtils.convert() method, it would be necessary to produce a great deal of source code that has a high level of redundancy.
It would be more elegant if you could configure the test cases. This means that the source code for the tests is automatically generated and executed based on a few input parameters.
However, in ServiceNow the use of the JavaScript method eval() is not recommended.
Instead, you can use the class GlideScopedEvaluator which does the same but in a more secure environment. However, there is a small problem here. You can't pass plain text with JavaScript code to this class. The method evaluateScript() requires a valid GlideRecord object from which the JavaScript code is read.
Fortunately, there is a workaround for that: Just read any record from any table and use it for wrapping your JavaScript code in an available text field. As long as you do not update the record, it is not a concern. In the following solution, I choose the field 'script' from a Business Rule record.
The final solution is as follows and I will explain all parts below:
(function(outputs, steps, stepResult, assertEqual) {
var objCurrencyUtils = new CurrencyUtils();
var gseEvaluator = new GlideScopedEvaluator();
var grBusinessRule = new GlideRecord('sys_script');
grBusinessRule.setLimit(1);
grBusinessRule.query();
grBusinessRule.next();
gseEvaluator.putVariable('objCurrencyUtils', objCurrencyUtils);
var arrTestCases = [
{
signature: ".convert('USD', 'EUR', '111')",
test: "objResult.error_message.length == 0",
explanation: "no error message"
}, {
signature: ".convert('USD', 'EURR', '111', 'de_DE')",
test: "objResult.error_message.length > 0",
explanation: "invalid currency 'EURR'"
}, {
signature: ".convert('USD', 'EUR', '111')",
test: "objResult.original_amount > objResult.converted_amount",
explanation: "amount in USD is larger than converted amount in EUR"
},
];
var getConvertResult = function(strSignature) {
grBusinessRule.setValue('script', 'objCurrencyUtils' + strSignature);
return gseEvaluator.evaluateScript(grBusinessRule, 'script');
};
var getTestResult = function(objResult, strTest) {
grBusinessRule.setValue('script', strTest);
gseEvaluator.putVariable('objResult', objResult);
return gseEvaluator.evaluateScript(grBusinessRule, 'script');
};
describe("CurrencyUtils", function() {
arrTestCases.forEach(function(objTestCase) {
it(objTestCase.signature + ' with ' + objTestCase.explanation, function () {
expect(true).toBe(
getTestResult(
getConvertResult(objTestCase.signature),
objTestCase.test
)
);
});
});
});
})(outputs, steps, stepResult, assertEqual);
jasmine.getEnv().execute();
Code | Explanation |
|
As there is no initialization within that class, an object can be instantiated once. This way it is available in all test cases. |
|
Instantiation of the script evaluator from ServiceNow. Furthermore, the CurrenyUtils object is passed to that evaluator to make it available when executing the test code. |
|
Loading of a single Business Rule record to act as a container for the JavaScript code to be executed |
|
Within that array all test cases can be specified:
|
|
These two functions execute the code to be tested, as well as the expectation. To execute the code, the "script" field of the Business Rule record is set and passed to the script evaluator. |
|
Inside this nested code block all test executions are defined based on the contents of the array arrTestCases |
The output after running that test step looks as follows:
You may wonder why all test cases have passed, as the code from the second test case is wrong:
.convert('USD', 'EURR', '111', 'de_DE')
The reason for that is the test expectation, which checks whether the returned object contains an error message or not:
objResult.error_message.length > 0
So we assume that the test case must fail ("EURR" is not a valid currency). And since this expectation is met, the test has passed.
Conclusion & Enhancements
With the help of the introduced pattern you are able to define many test cases in a short time and with little effort. Just extend the array arrTestCases with another elements.
For the organization of all test cases, I recommend using individual ATF tests for each Script Include and individual ATF test steps for each method.
To minimize the resulting code redundancy, it is advisable to outsource the pattern described in this article into a Script Include and thus make it reusable.
- 3,835 Views
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Thanks for sharing :))