When developing a Component Service (i.e., Custom Component or Entity Event Handler) using the bots-node-sdk (https://github.com/oracle/bots-node-sdk), you will most likely find yourself wanting to do some unit testing and debugging before pushing to ODA. In the GitHub repository, you will see a section on testing that leverages Jasmine (https://jasmine.github.io/). However, if you are old-school and a visual person like myself, having the ability to run a test locally with logging statements goes a long way. This blog post will focus on the testing section of bots-node-sdk but use plain-ole-javascript for the unit tester instead of Jasmine assertions.
In an earlier post entitled Oracle Digital Assistant (ODA) Component Service Testing via Node.js, details were provided on how to test ODA Custom Components. In this post, our focus will be on the Entity Event Handlers (EEH) where processing happens via events associated with an ODA composite bag and its items.
Anatomy of the Testing Script
The tester script that we will be looking at contains the following sections:
- Pull in the bots-node-sdk testing modules and the EEH we are testing
- Setup the composite bag and associated entity items
- Create a MockEventHandlerRequest containing ODA variables and events
- Create the ODA JSON variables which include the composite bag created earlier
- Create the ODA JSON events including their properties
- Create the MockEventHandlerRequest using the JSON variables and events
- Set the various loggers in the component context
- Create a ComponentShell that will be used to resolve the entities (generate events) in the composite bag
- Generate the events using the ComponentShell object and MockEventHandlerRequest object
The require(...) Function
For our simple tester, we will have the following three lines to get the appropriate module/code into our script:
const { ComponentRegistry, ComponentShell } = require('@oracle/bots-node-sdk/lib');
const Testing = require('@oracle/bots-node-sdk/testing');
const Component = require('../components/eeh_translate2pirate');
These four constants will provide access to the necessary testing objects and the EEH.
The Composite Bag
Entity Event Handlers require a composite bag to work against, and that is what we are setting up first. A composte bag will contain all related variables (aka, entities) for some use case. These variables are “resolved” by user input or programmatically via the EEH. In our example, we will have three variables in the bag where one is set by user input and the other two are set by the EEH. This is what the code looks like when setting up the bag:
var items = [];
items.push(Testing.MockCompositeBagItem("what2Translate", "STRING"));
items.push(Testing.MockCompositeBagItem("pirateTranslation", "STRING"));
items.push(Testing.MockCompositeBagItem("translationJSON", "STRING"));
The MockEventHandlerRequest
Setting up the MockEventHandlerRequest is an essential part of the unit tester code. It is here that we define all the pieces of data that the EEH will need. The MockEventHandlerRequest function takes six parameters: variableName, currentItem, userMessage, candidateMessage, events, and variables. Before we can create the MockEventHandlerRequest we need to create the general ODA variables needed for processing and the events that will be raised. Once we have these two JSON objects, the MockEventHandlerRequest can be created:
//================================================================================
// Setup the ODA variables, event properties, event list, and request objects
//================================================================================
let odaVariables = {
'system.config.PIRATEURL': {
'type': 'STRING',
'entity': false,
'value': 'https://api.funtranslations.com/translate/pirate.json?text={PHRASE}'
},
'exampleNumber': {
'type': 'NUMBER',
'entity': false,
'value': 8
},
'pirateSpeak': Testing.MockCompositeBagEntityVariable('PirateSpeak', items),
};
let entityPromptProp1 = { promptCount: 1, currentItem: 'what2Translate'};
let what2TranslateProp1 = { newValue: 'Fire the cannon from the bow of the boat!', oldValue: null };
let events = [
Testing.MockResolveEntitiesEvent('publishMessage', false, entityPromptProp1),
Testing.MockResolveEntitiesEvent('validate', false, what2TranslateProp1, 'what2Translate')
];
let request = Testing.MockEventHandlerRequest('pirateSpeak',
'what2Translate',
'Fire the cannon from the bow of the boat!',
'',
events,
odaVariables);
Set the Loggers
It’s important to understand that various loggers are available for debug, error, fatal, info, trace, and warn levels/scenarios. For our tester, we will use the same logger (i.e., the console):
//================================================================================ // Setup the Logging Levels // Comment/Uncomment the various logger levels to control the amount of logging details to the console // NOTE: The debug logger will produce various SDK log messages //================================================================================ var logger = Testing.MockConversation.fromRequest(Testing.MockRequest()).logger(); logger.debug = console.log; logger.error = console.log; logger.fatal = console.log; logger.info = console.log; logger.trace = console.log; logger.warn = console.log;
Create the ComponentShell and Generate Events
We are now ready to resolve the entities by raising the events we defined above. The way we do this is by creating a ComponentShell and then using that shell to resolve the entities using the EEH:
//================================================================================
// Create Component Shell and Resolve Entities (i.e., Generate Events)
//================================================================================
var registry = ComponentRegistry.create(Component);
var shell = ComponentShell(null, registry);
shell.invokeResolveEntitiesEventHandler('eeh_translate2pirate', request, (err, res) => {
if (err != undefined) {
console.log(err);
}
if (res != undefined) {
console.log("Testing: res ...\n"+JSON.stringify(res, null, 2));
}
});
Once the invokeResolveEntitiesEventHandler method returns, we can access things in the err/res objects. In our example, we print out the entire err/res so we can see the big picture.
Execute the Tester
The tester script now can be run via the command-line using Node.js (i.e., the node command), If you need to debug, you can set breakpoints in your code and run the above command in a JavaScript Debug Terminal window. Here is the output in the terminal window for our example:
$ node unit-testers/eeh_translate2pirate_tester.js
Invoking event handler entity.publishMessage with event: {"name":"publishMessage","custom":false,"properties":{"promptCount":1,"currentItem":"what2Translate"}}
Using candidate bot messages
publishMessage returned true
Invoking event handler what2Translate.validate with event: {"name":"validate","custom":false,"properties":{"newValue":"Fire the cannon from the bow of the boat!","oldValue":null},"eventItem":"what2Translate"}
EEH: what2Translate.validate ...
{
"newValue": "Fire the cannon from the bow of the boat!",
"oldValue": null
}
EEH: fetchResult.status: 200
EEH: translation:
{
"success": {
"total": 1
},
"contents": {
"translated": "Fire th' cannon from th' bow o' th' boat!",
"text": "Fire the cannon from the bow of the boat!",
"translation": "pirate"
}
}
SDK: About to set variable pirateSpeak
SDK: Setting variable {"type":{"name":"PirateSpeak","type":"COMPOSITEBAG","compositeBagItems":[{"name":"what2Translate","type":"STRING"},{"name":"pirateTranslation","type":"STRING"},{"name":"translationJSON","type":"STRING"}]},"value":{"entityName":"PirateSpeak"},"entity":true}
EEH: what2Translate.validate context...
{
"_request": {
"botId": "mockbot",
"platformVersion": "1.1",
"context": {
"variables": {
"system.config.PIRATEURL": {
"type": "STRING",
"entity": false,
"value": "https://api.funtranslations.com/translate/pirate.json?text={PHRASE}"
},
"exampleNumber": {
"type": "NUMBER",
"entity": false,
"value": 8
},
"pirateSpeak": {
"type": {
"name": "PirateSpeak",
"type": "COMPOSITEBAG",
"compositeBagItems": [
{
"name": "what2Translate",
"type": "STRING"
},
{
"name": "pirateTranslation",
"type": "STRING"
},
{
"name": "translationJSON",
"type": "STRING"
}
]
},
"value": {
"entityName": "PirateSpeak",
"pirateTranslation": "Fire th' cannon from th' bow o' th' boat!",
"translationJSON": {
"success": {
"total": 1
},
"contents": {
"translated": "Fire th' cannon from th' bow o' th' boat!",
"text": "Fire the cannon from the bow of the boat!",
"translation": "pirate"
}
}
},
"entity": true
}
}
},
"variableName": "pirateSpeak",
"candidateMessages": [
{
"type": "text",
"text": ""
}
],
"events": [
{
"name": "publishMessage",
"custom": false,
"properties": {
"promptCount": 1,
"currentItem": "what2Translate"
}
},
{
"name": "validate",
"custom": false,
"properties": {
"newValue": "Fire the cannon from the bow of the boat!",
"oldValue": null
},
"eventItem": "what2Translate"
}
],
"entityResolutionStatus": {
"updatedEntities": [],
"outOfOrderMatches": [],
"customProperties": {},
"shouldPromptCache": {},
"validationErrors": {},
"skippedItems": [],
"allMatches": [],
"resolvingField": "what2Translate",
"userInput": "Fire the cannon from the bow of the boat!",
"disambiguationValues": {}
}
},
"_response": {
"context": {
"variables": {
"system.config.PIRATEURL": {
"type": "STRING",
"entity": false,
"value": "https://api.funtranslations.com/translate/pirate.json?text={PHRASE}"
},
"exampleNumber": {
"type": "NUMBER",
"entity": false,
"value": 8
},
"pirateSpeak": {
"type": {
"name": "PirateSpeak",
"type": "COMPOSITEBAG",
"compositeBagItems": [
{
"name": "what2Translate",
"type": "STRING"
},
{
"name": "pirateTranslation",
"type": "STRING"
},
{
"name": "translationJSON",
"type": "STRING"
}
]
},
"value": {
"entityName": "PirateSpeak",
"pirateTranslation": "Fire th' cannon from th' bow o' th' boat!",
"translationJSON": {
"success": {
"total": 1
},
"contents": {
"translated": "Fire th' cannon from th' bow o' th' boat!",
"text": "Fire the cannon from the bow of the boat!",
"translation": "pirate"
}
}
},
"entity": true
}
}
},
"error": false,
"validationResults": {},
"keepProcessing": false,
"cancel": false,
"modifyContext": true,
"entityResolutionStatus": {
"updatedEntities": [],
"outOfOrderMatches": [],
"customProperties": {},
"shouldPromptCache": {},
"validationErrors": {},
"skippedItems": [],
"allMatches": [],
"resolvingField": "what2Translate",
"userInput": "Fire the cannon from the bow of the boat!",
"disambiguationValues": {}
},
"messages": [
{
"type": "text",
"text": ""
}
]
},
"_logger": {},
"_entityStatus": {
"updatedEntities": [],
"outOfOrderMatches": [],
"customProperties": {},
"shouldPromptCache": {},
"validationErrors": {},
"skippedItems": [],
"allMatches": [],
"resolvingField": "what2Translate",
"userInput": "Fire the cannon from the bow of the boat!",
"disambiguationValues": {}
},
"_entity": {
"entityName": "PirateSpeak",
"pirateTranslation": "Fire th' cannon from th' bow o' th' boat!",
"translationJSON": {
"success": {
"total": 1
},
"contents": {
"translated": "Fire th' cannon from th' bow o' th' boat!",
"text": "Fire the cannon from the bow of the boat!",
"translation": "pirate"
}
}
},
"_systemEntityDisplayProperties": {
"EMAIL": {
"properties": [
"email"
]
},
"CURRENCY": {
"properties": [
"amount",
"currency"
]
},
"NUMBER": {
"properties": [
"number"
]
},
"YES_NO": {
"properties": [
"yesno"
]
},
"DATE": {
"properties": [
"date"
]
},
"TIME": {
"properties": [
"originalString"
]
},
"DURATION": {
"properties": [
"startDate",
"endDate"
]
},
"ADDRESS": {
"properties": [
"originalString"
]
},
"PERSON": {
"properties": [
"originalString"
]
},
"PHONE_NUMBER": {
"properties": [
"completeNumber"
]
},
"SET": {
"properties": [
"originalString"
]
},
"URL": {
"properties": [
"fullPath"
]
},
"ATTACHMENT_ITEM": {
"properties": [
"url"
]
},
"LOCATION_ITEM": {
"properties": [
"latitude, longitude"
]
}
}
}
validate returned true
Testing: res ...
{
"context": {
"variables": {
"system.config.PIRATEURL": {
"type": "STRING",
"entity": false,
"value": "https://api.funtranslations.com/translate/pirate.json?text={PHRASE}"
},
"exampleNumber": {
"type": "NUMBER",
"entity": false,
"value": 8
},
"pirateSpeak": {
"type": {
"name": "PirateSpeak",
"type": "COMPOSITEBAG",
"compositeBagItems": [
{
"name": "what2Translate",
"type": "STRING"
},
{
"name": "pirateTranslation",
"type": "STRING"
},
{
"name": "translationJSON",
"type": "STRING"
}
]
},
"value": {
"entityName": "PirateSpeak",
"pirateTranslation": "Fire th' cannon from th' bow o' th' boat!",
"translationJSON": {
"success": {
"total": 1
},
"contents": {
"translated": "Fire th' cannon from th' bow o' th' boat!",
"text": "Fire the cannon from the bow of the boat!",
"translation": "pirate"
}
}
},
"entity": true
}
}
},
"error": false,
"validationResults": {
"what2Translate.validate": true
},
"keepProcessing": false,
"cancel": false,
"modifyContext": true,
"entityResolutionStatus": {
"updatedEntities": [],
"outOfOrderMatches": [],
"customProperties": {},
"shouldPromptCache": {},
"validationErrors": {},
"skippedItems": [],
"allMatches": [],
"resolvingField": "what2Translate",
"userInput": "Fire the cannon from the bow of the boat!",
"disambiguationValues": {}
},
"messages": [
{
"type": "text",
"text": ""
}
]
}
Source Screenshots
Unfortunately, it is not possible to provide the code but the following are what the source files look like for your reference:
I hope you find this helpful and if you need something similar for Custom Components, see my complimentary blog Oracle Digital Assistant (ODA) Component Service Testing via Node.js.
