Building a Starship Computer with TDD and Bespoken’s Virtual Alexa

Marc Courquin
11 min readApr 1, 2018

--

Starship Captain

Alexa Skill. Live version. on Amazon or on Amazon UK.

The Goal

The idea is to take a TDD approach to building an Alexa Skill that will provide an experience of being on a starship and interacting with the ships’s computer. The user should be able to say things like, beam me up, raise shields, fire phasers, engage warp, etc, and the ships computer should respond accordingly.

But it should be something more than just a few stale statements and an equally stale response. It should feel like a real conversation, there should be a story and there should be sound effects.

As a starting point Bespoken has a number of solutions to help and support voice application development. One of these is Virtual Alexa, a SDK, that supports unit-testing and regression testing of Alexa skill apps. Just the ticket for this TDD adventure. So what else is on the shopping list?

Full Shopping List

Alexa Skills Kit SDK for Node.js
Chai Assertion Library
Mocha Testing Framework
Node
Virtual Alexa

Setting up the Environment

First setup the environment

$ mkdir Starship
$ cd Starship
$ npm init -y
$ npm install mocha --save-dev
$ npm install chai --save-dev
$ npm install virtual-alexa --save-dev
$ npm install alexa-sdk --save
$ touch index.js

Setup index.js with some boilerplate starting point code.

'use strict';const Alexa = require("alexa-sdk");
exports.handler = (event, context) => {
const alexa = Alexa.handler(event, context);
alexa.appId = '';
alexa.registerHandlers(newSessionHandler);
alexa.execute();
};
// The newSessionHandlers only includes an Unhandled intent.
// This will catch and highlight all missing intents
const newSessionHandlers = {
Unhandled() {
const requestName =
(this.event.request.intent) ?
this.event.request.intent.name :
this.event.request.type;
this.response.speak(
`
Unhandled.
INTENT.['${requestName}' Missing.
STATE. [${this.handler.state}].
ATTRIBUTES. = ${this.attributes}
`
);
this.emit(':responseReady');
}
};

Setup the test bed environment.

Directories and files for virtual-alexa. The file ‘IntentSchema.json’ will hold the intents. The file ‘SampleUtterances.txt’ will hold the text which identifies the intents. This is explained in more detail below.

$ mkdir VirtualAlexa
$ touch VirtualAlexa/IntentSchema.json
$ touch VirtualAlexa/SampleUtterances.txt

Virtual Alexa expects the ‘IntentSchema.json’ file to have an array of intents. If this is not set up Virtual Alexa will throw an error when it is instantiated in the test bed, therefore we set it up with an empty array.

{
"intents": []
}

Directories and files for mocha. The file ‘test.js’ will hold all of the test cases

$ mkdir test
$ touch test/test.js

Setup ‘test.js’ with some boilerplate starting point code.

/*eslint-env mocha */
'use strict';
// setup chai should
const should = require("chai").should();
// setup Virtual Alexa
// user empty intent array in IntentSchema.json
// and empty utterances in SampleUtterances.txt
const vax = require("virtual-alexa");
const alexa = vax.VirtualAlexa.Builder()
.handler("index.handler") // Lambda function file and name
.intentSchemaFile("./VirtualAlexa/IntentSchema.json")
.sampleUtterancesFile("./VirtualAlexa/SampleUtterances.txt")
.create();
// Place holder to hold virtual alexa's response
let reply = '';
describe('Starship Captain', () => { describe('When starting the game', () => { // Launch Virtual Alexa before tests
// All enclosed tests will be run in the same session
before(async ()=> {
reply = await alexa.launch();
});
});
});

The boilerplate code above has set up the basic starting point. Running mocha give us the following response

$ node_modules/.bin/mocha    0 passing (1ms)$ 

Not much to look at, but the work to date provides a solid and stable starting point, so let’s get started.

Getting Started

Let’s write the first test, the starship computer should welcome the user, as the captain, when the game begins. The mocha describe function ‘When starting the game’ already exists and the before function has launched virtual alexa. Below is the first test

...// Launch Virtual Alexa before tests
before(async ()=> {
reply = await alexa.launch();
});
// the response is held in the reply.response.outputSpeech.ssml
// check it matches our expected response
it('should welcome the captain', () => {
reply.response.outputSpeech.ssml
.should.include("welcome back captain");
}

If mocha is run

$ node_modules/.bin/mocha    Starship Captain
When starting the game
Warning: Application ID is not set
1) should welcome the captain
0 passing (114ms)
1 failing
1) Starship Captain
When starting the game
should welcome the captain:

AssertionError: expected '<speak> \n Unhandled.\n
INTENT.[\'LaunchRequest\' Missing.\n STATE. []. \n
ATTRIBUTES. = [object Object]\n </speak>' to include
'welcome back captain'
at Context.it (test/test.js:18:53)
at <anonymous>
at process._tickDomainCallback
(internal/process/next_tick.js:228:7)
$

There is an unhandled ‘LaunchRequest’. When starting an Alexa skill a ‘LaunchRequest’ event occurs. The event can be handled by adding a ‘LaunchRequest’ handler and within the handler speaking the expected response welcoming the captain. The file ‘index.js’ can be updated as below

...
const newSessionHandlers = {
LaunchRequest() {
this.response
.speak(
`welcome back captain.
transporters are online and ready for transport.`)
.listen('Would you like me to transport you to the ship?');
this.emit(':responseReady');
},
Unhandled() {
...

After the update, running mocha again give a successful response.

$ node_modules/.bin/mochaStarship Captain
When starting the game
Warning: Application ID is not set
✓ should welcome the captain
1 passing (127ms)

To make a more real and less stale conversation, a hint and confirmation conversation pattern is used, so rather than saying ‘welcome back captain, do you want to transport?’ and expecting a yes or no response the speech output is ‘welcome back captain, transporters are online and ready for transport’. This hints and highlights the transporters and it is quite natural for the user to respond ‘transport me’ or, if they have watched too much Star Trek, ‘beam me up’. If there is no response from the user within a timeout period the user will be prompted directly with the yes or no question. The user’s ‘transport me’ or ‘beam me up’ will be the first intent.

Coding the first intent test case

If the user responds with ‘beam me up’ then the Starship computer should respond with something like ‘energizing’. Let’s test this intent.

...it('should transport the captain if requested', async () => {
reply = await alexa.utter('beam me up');
reply.response.outputSpeech.ssml
.should.include('energizing.');
});
...

and then run mocha

$node_modules/.bin/mocha...1 failing1) Starship Captain
When starting the game
should transport the captain if requested:
TypeError: Cannot read property '0' of undefined
at SampleUtterances.defaultUtterance (node_modules/virtual-core/lib/src/SampleUtterances.js:27:41)

It fails as expected. The error is a bit cryptic but looking carefully at the response reveals that the issue is with the utterances and with a bit of a logical jump its clear that the utterances and intents need to be updated.

Firstly adding the ‘TransportToShipIntent’ to the intents file ‘IntentSchema.json’

{
"intents": [
{ "intent": "TransportToShipIntent" }
]
}

and then mapping the ‘beam me up’ and ‘transport me’ utterances to the ‘TransportToShipIntent’ in the file ‘SampleUtterances.txt’ should sort things out.

TransportToShipIntent beam me up
TransportToShipIntent transport me

Running mocha gives the following response

$ node_modules/.bin/mocha...
1 failing
1) Starship Captain
When starting the game
should transport the captain if requested:
AssertionError: expected '<speak> \n Unhandled.\n INTENT.[\'TransportToShipIntent\' Missing.\n STATE. []. \n ATTRIBUTES. = [object Object]\n </speak>' to include 'energizing.'

The starship computer has recognized the intent but doesn’t know what to do. An update to the ‘index.js’ is required.

LauchRequest() {
...
},
TransportToShipIntent() {
this.response
.speak(
`energizing. welcome aboard captain.
new orders have been received from alliance command.`)
.listen(
`would you like me to read you the orders
from alliance command?`);
this.emit(':responseReady');
},
...

Running mocha now gives success.

node_modules/.bin/mochaStarship Captain
When starting the game
Warning: Application ID is not set
✓ should welcome the captain
Warning: Application ID is not set
Warning: Application ID is not set
✓ should transport the captain if requested
2 passing (291ms)

A short summary of where we are

The starship computer is coming along. It welcomes the user as the captain when it is launched and when the user requests to ‘beam up’ the computer recognizes the intent and promptly transports the captain. Thats all very nice but we need to talk about state.

State

It might not have been noticeable but something changed when the computer transported the user. One moment the user was wherever they were at the beginning of the game and then after being transported the user was aboard the starship. These changes in circumstances, or changes in state, should affect the way the computer responds to the users requests.

Consider if the user requests to raise shields, if the user is not on board the ship then the computer may respond by telling the user that shields can not be raised because the captain is not on board. but if the user is onboard then the computer should respond by raising the shields.

Let’s go ahead and add the test case that the starship computer should tell the captain that it is unable to comply wen asked to raise the shields to ‘test.js’ .

...it('should not raise the shields if asked', async () => {

reply = await alexa.utter('shields up');
reply.response.outputSpeech.ssml
.should.include('unable to comply');
});
....

Running mocha results

$ node_modules/.bin/mocha...No intentName matches utterance: shields up. Using fallback utterance: beam me up
Warning: Application ID is not set
1) should not raise the shields if asked
2 passing (122ms)
1 failing
1) Starship Captain
When starting the game
should not raise the shields if asked:
AssertionError: expected '<speak> energizing. welcome aboard captain. new orders have been received from alliance command. </speak>' to include 'unable to comply'

This time virtual Alexa actually gives provides a warning that there is no matching intent and the test fails. The ‘sampleUtterances.txt’ and ‘intentSchema.json’ files need to be updated to make the test case succeed.

update samleUtterances.txt

TransportToShipIntent beam me up
TransportToShipIntent transport me
RaiseShieldsIntent raise shields
RaiseShieldsIntent shields up

update intentSchema.json

{
"intents": [
{
"intent": "TransportToShipIntent"
},
{
"intent": "RaiseShieldsIntent"
}

]
}

The unhandled event in ‘index.js’ picks up that the RaiseShieldsIntent is not registered and the the test fails when mocha is run.

$ node_modules/.bin/mocha...1) should not raise the shields if asked2 passing (119ms)
1 failing
1) Starship Captain
When starting the game
should not raise the shields if asked:
AssertionError: expected '<speak> \n Unhandled.\n INTENT.[\'RaiseShieldsIntent\' Missing.\n STATE. []. \n ATTRIBUTES. = [object Object]\n </speak>' to include 'unable to comply'

Update index.js and add the event handler for RaiseShieldsIntent

...RaiseShieldsIntent() {
this.response.speak('unable to comply');
this.emit(':responseReady');
},
...

Now running mocha results in success.

$ node_modules/.bin/mochaStarship Captain
When starting the game
Warning: Application ID is not set
✓ should welcome the captain
Warning: Application ID is not set
Warning: Application ID is not set
✓ should transport the captain if requested
Warning: Application ID is not set
Warning: Application ID is not set
✓ should not raise the shields if asked
3 passing (120ms)

The next step is to have the computer raise the shields if the captain is on board but before doing that the code needs to be updated to track whether the Captain is onboard or not. This is where state comes in and it can be read or set by accessing the ‘this.handler.state’ variable.

A good place to set the state to onboard is within the TransportToShipIntent handler within index.js.

TransportToShipIntent() {
this.handler.state = `_OnBoardShip`;
this.response
.speak('energizing. welcome aboard captain. new orders have
...},

Check everything is still ok by running mocha.

$ node_modules/.bin/mocha...1) should not raise the shields if asked2 passing (124ms)
1 failing
1) Starship Captain
When starting the game
should not raise the shields if asked:
Error: In state: _OnBoardShip. No handler function was defined for event RaiseShieldsIntent and no 'Unhandled' function was defined.

What happened?

The reason is that so far we have been running in the ‘default state’ since starting the starship’s computer and now we have two states, the ‘default state’ which is none ( state = ‘’ ) and the new ‘_OnBoardShip’ state but there is only one state handler, the default one. To make things right another state handler is needed to cope with the intents that occur in the ‘_OnBoardShip’ state.

However before doing that, it’s a good time to talk about Session

Session. A small diversion.

When a user starts an Alexa Skill a new session is created and any changes to the state are reflected in the current session. In the test cases so far the LaunchRequest starts the new session, the TransportToShipIntent updates the state from the default ‘’ to ‘_OnBoardShip’ and updates the current Session’s state to ‘_OnBoardShip’ so on the RaiseShieldsIntent an error occurs because the current Session’s state, ‘_OnBoardShip’ has no handler.

The error can be avoided by changing the sequence of intents by making the RaiseShieldsIntent come before the TransportToShipIntent because the state will still be the default one and the default handler has a handler for RaiseShieldsIntent.

Let’s go ahead and make the change to test.js.

const newSessionHandlers = {
...
RaiseShieldsIntent ... TransportToShipIntent() ......

running mocha gives success

$ node_modules/.bin/mochaStarship Captain
When starting the game
Warning: Application ID is not set
✓ should welcome the captain
Warning: Application ID is not set
✓ should not raise the shields if asked
Warning: Application ID is not set
✓ should transport the captain if requested
3 passing (112ms)

The error could also be avoided by running each test in its own new session which will have a default ‘’ state. This can be done by changing the before() function in test.js to beforeEach().

...describe('When starting the game', () => {
beforeEach(async ()=> {
reply = await alexa.launch();
});
...

running mocha like this also gives success even with the original order but notice how the Application Id warnings are doubled because a new session is created each lime by the LaunchRequests.

$ node_modules/.bin/mochaStarship Captain
When starting the game
Warning: Application ID is not set
✓ should welcome the captain
Warning: Application ID is not set
Warning: Application ID is not set

✓ should transport the captain if requested
Warning: Application ID is not set
Warning: Application ID is not set

✓ should not raise the shields if asked
3 passing (112ms)

Back on track.

Either because of the sequence change in testing the intents or because of running each intent in a new session all of the ‘When starting the game’ tests are passing. Lets write a new raise shields test for when the Captain is onboard. In the new test the initial state after launch is set to ‘_OnBoardShip’.

describe('When the Captain is onboard the ship', () => {

before(async ()=>{
reply = await alexa.launch();
alexa.context().session()
.attributes()['STATE'] = '_OnBoardShip';
});
it('should raise the shields if asked', async () => {
reply = await alexa.utter('shields up');
reply.response.outputSpeech.ssml
.should.include('shields raised');
});
});

Running mocha results in the same error as before but our understanding is hopefully more clear.

3 passing (123ms)
1 failing
1) Starship Captain
When the Captain is onboard the ship
should raise the shields if asked:
Error: In state: _OnBoardShip. No handler function was defined for event RaiseShieldsIntent and no 'Unhandled' function was defined.

A new ‘_OnBoardShip’ state handler

Looking at the changes to index.js shows that the syntax for writing a none default state handler is slightly different. A change has also been made to the registerHandlers to include the new _OnBoardShipHandlers.

'use strict';
const Alexa = require("alexa-sdk");
exports.handler = (event, context) => {
const alexa = Alexa.handler(event, context);
alexa.appId = '';
alexa.registerHandlers(
newSessionHandlers,
_OnBoardShipHandlers

);
alexa.execute();
};
...const _OnBoardShipHandlers =
Alexa.CreateStateHandler('_OnBoardShip', {
RaiseShieldsIntent() {
this.response.speak('shields raised');
this.emit(':responseReady');
},
Unhandled() {
this.response.speak(`_OnBoardShip Handler. Intent
'${this.event.request.intent.name}'is not handled.
STATE equals
[${this.handler.state}]. Attributes = ${this.attributes}`);
this.emit(':responseReady');
}

}):

Running mocha gives success for both the starting and onboard tests.

$ node_modules/.bin/mochaStarship Captain
When starting the game
Warning: Application ID is not set
✓ should welcome the captain
Warning: Application ID is not set
Warning: Application ID is not set
✓ should transport the captain if requested
Warning: Application ID is not set
Warning: Application ID is not set
✓ should not raise the shields if asked
When the Captain is onboard the ship
Warning: Application ID is not set
Warning: Application ID is not set
✓ should raise the shields if asked
4 passing (113ms)

Because the state is explicitly set at launch in the Captain onboard tests we can run them independently from the starting tests using mocha’s pattern matching option

$ node_modules/.bin/mocha -g 'onboard'Starship Captain
When the Captain is onboard the ship
Warning: Application ID is not set
Warning: Application ID is not set
✓ should raise the shields if asked
1 passing (110ms)

Thats all for now. I hope you have found it useful.
As a follow-up to this article I plan to cover conversation patterns and audio,

The final solution has been published as an Alexa skill named
‘Starship Captain’ and can be found on Amazon or on Amazon UK.

Do tell me what you think.

Happy Developing …

--

--