By Josh Skeen, software developer at Big Nerd Ranch
This is part one of the Big Nerd Ranch series. Read about our free developer training for Alexa Skills Kit here.
If you want to build Alexa Skills, where should you start? You could begin with building one of the sample skills like the color picker or the trivia game. But when you’ve already tackled “Hello, World,” you’re ready to dive in.
Not quite yet. First, let’s set up a local development environment. Why should you use a local development environment over testing on a live server? There are many benefits. Chief among them are the fact that you gain access to the debugger and the stack trace, and you can quickly test changes without uploading files to a remote server, cutting down your iteration time.
In addition to time considerations, there are other concerns: what if the network is running slowly, you’re on a plane, or the Wi-Fi isn’t working? With a local dev environment, you can still get work done.
That’s where this post comes in: it will guide you through setting up a local development environment so that you can work more efficiently, enabling you to rapidly test your skills as you develop them. We will first set up a working environment with Node.js, and then we will build a model for our Alexa Skill. This skill—Airport Info—will track airport flight delays and weather conditions, and will give us a chance to try developing a more complex Alexa Skill locally.
We will begin by building a model for the Airport Info skill. For this blog post, we're going to use Node.JS and Amazon's AWS Lambda compute service. You have the flexibility to use other languages with Lambda or even any HTTPS endpoint, but we chose Node.js here because it’s easy to get started with and is widely used in the Alexa Skill development community.
The source code for this project is available on GitHub. Let’s set up our environment.
Before we can begin developing on our Node.js-backed Alexa skill locally, we will of course need to install Node.js. To get started, I’d suggest pulling down Node Version Manager (nvm) to easily keep track of different versions of Node.
Let’s begin by installing nvm. Note that NVM does not exist for a Windows environment. There is an alternative available, with a slightly different set of commands (nvm list vs nvm ls, for example).
Your home directory may appear differently based on your operating system. We will use "local ✗" as a generic home command prompt.
-> local ✗ curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.30.2/install.sh | bash
Close and reopen the terminal to ensure the NVM binary is loaded on the classpath, and verify that you’ve got NVM installed by typing:
nvm ls
You should see something similar to the following:
-> local ✗ nvm ls -> system
Our skill will live on Amazon’s AWS Lambda Service, so we want to ensure our local environment matches the AWS Lambda environment. Though this may change, at the time of this writing, AWS Lambda currently supports one version of Node.js: v0.10.36.
While today this means we’ll be working with ES5, in a future article we’ll explore enabling ES6 support.
Install Node v0.10.36 using the following command:
nvm install v0.10.36
After this completes, set the default version of Node so we won’t have to fiddle with it in the future:
➜ localhost ✗ nvm alias default v0.10.36 default -> v0.10.36
Included with Node.js is the node package manager (npm), which we’ll use to add the dependencies our skill needs. Create a new directory for the project called faa-info. This is where our skill service code will live.
Open a terminal window and go to this directory. Next, we will initialize a new package.json file to hold a list of dependencies our project will use:
npm init
Run through the dialogs, accepting the defaults for each dialog, and when you’re prompted to enter a test command, type:
mocha
We will use Chai and Mocha, two JavaScript assertion and test libraries, to build our tests, so we're adding them while we set up npm.
Now that we've configured npm as our test runner, we will add all of the dependencies the project will use. Add the required dependencies to our package.json:
npm install --save alexa-app chai chai-as-promised mocha lodash request-promise
npm should download these dependencies and add them to the package.json file. A new “node_modules” directory containing the dependencies should appear.
If everything was successfully added to your project, your final package.json should read as follows. Note that the version numbers may be slightly different:
{ "name": "faa-info", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "mocha" }, "author": "", "license": "ISC", "dependencies": { "alexa-app": "^2.3.2", "alexa-app-server": "^2.2.4", "chai": "^3.5.0", "chai-as-promised": "^5.2.0", "lodash": "^4.5.0", "mocha": "^2.4.5", "request-promise": "^2.0.1" } }
Now we’re ready to begin building our skill.
Let’s consider the feature our skill offers: in response to an IATA airport code provided by the user, our skill will give information relevant for that particular airport.
The service we’ll use is the FAA’s public airport status service. The endpoint accepts an IATA airport code and a format, like http://services.faa.gov/airport/status/SFO?format=application/json to get information about the San Francisco airport. If you visit that URL, you will see the information we would like our skill to ultimately read back to the user: delay status, weather conditions and temperature info. The FAA payload contains all of this:
{"delay":"false","IATA":"SFO","state":"California","name":"San Francisco International","weather":{"visibility":9.00,"weather":"Mostly Cloudy","meta":{"credit":"NOAA's National Weather Service","updated":"9:56 AM Local","url":"http://weather.gov/"},"temp":"58.0 F (14.4 C)","wind":"Northeast at 4.6mph"},"ICAO":"KSFO","city":"San Francisco","status":{"reason":"No known delays for this airport.","closureBegin":"","endTime":"","minDelay":"","avgDelay":"","maxDelay":""," closureEnd":"","trend":"","type":""}}
To proceed, we’ll first address the problem of requesting the data, then tackle formatting it so that Alexa can use it as a response.
Let’s start with some unit tests to ensure that the model works as we expect. Create a directory called test within faa-info and add a file called test_faa_data_helper.js. As I mentioned earlier, we will use Chai and Mocha to build our tests. They should already be installed, since we added them earlier via npm.
Your project directory should now look like this:
/faa-info *package.json* /test *test_faa_data_helper.js* /node_modules /alexa-app /chai /chai-as-promised /lodash /mocha /request-promise
Enter the following text in the test_faa_data_helper.js file you’ve just created.
'use strict'; var chai = require('chai'); var expect = chai.expect; var FAADataHelper = require('../faa_data_helper'); describe('FAADataHelper', function() { var subject = new FAADataHelper(); });
Our test will describe the behavior of the FAADataHelper class. The test doesn’t yet make any assertions, but it does require a helper class. It should fail, because we don’t have a helper class created yet.
We can now run our test with mocha test/test_faa_data_helper.js from the terminal in the root of our project.
-> localhost ✗ npm test module.js:340 throw err; ^ Error: Cannot find module '../faa_data_helper'
It looks like our test file ran, but couldn’t find the module because we haven’t created it. Let’s create a new file called faa_data_helper.js in the root directory (../faa-info) and start a module called FAADataHelper.
'use strict'; function FAADataHelper() { } module.exports = FAADataHelper;
Now let’s see what we get:
➜ localhost ✗ npm test 0 passing (2ms)
We need to make some assertions in our test. Our first test should check a made-up method called requestAirportStatus(airportCode) on FAADataHelper, which will eventually make the actual request to the Airport Info service, then return the JSON we saw earlier.
Update test_faa_data_helper.js with the following changes:
'use strict'; var chai = require('chai'); var chaiAsPromised = require('chai-as-promised'); chai.use(chaiAsPromised); var expect = chai.expect; var FAADataHelper = require('../faa_data_helper'); chai.config.includeStack = true; describe('FAADataHelper', function() { var subject = new FAADataHelper(); var airport_code; describe('#getAirportStatus', function() { context('with a valid airport code', function() { it('returns matching airport code', function() { airport_code = 'SFO'; var value = subject.requestAirportStatus(airport_code).then(function(obj) { return obj.IATA; }); return expect(value).to.eventually.eq(airport_code); }); }); }); });
This test asserts that a Promise-based network request returns a value we expect. (Our project will use request-promise.)
Notice the eventually portion of the matcher in use above? This is a special part of the chai-as-promised matcher we added, which allows us to make assertions about the data a Promise returns without requiring the use of callbacks, "sleeps" or other less-than-ideal approaches to waiting for the request to complete. For more in-depth coverage, Jani Hartikainen has written a great article on testing Promises.
If we run our test, we should see something like this:
FAADataHelper #getAirportStatus with a valid airport code 1) returns matching airport code 0 passing (8ms) 1 failing 1) FAADataHelper #getAirportStatus with a valid airport code returns airport code: TypeError: subject.requestAirportStatus is not a function
Failure. requestAirportStatus is not a function—so let’s make it one! After adding a boilerplate requestAirportStatus() method, our FAADataHelper class looks like this:
'use strict'; var _ = require('lodash'); var rp = require('request-promise'); var ENDPOINT = 'http://services.faa.gov/airport/status/'; function FAADataHelper() { } FAADataHelper.prototype.requestAirportStatus = function(airportCode) { }; module.exports = FAADataHelper;
Running the test again, we get a different failure:
1) FaaDataHelper #getAirportStatus with a valid airport code returns airport code: TypeError: Cannot read property 'then' of undefined
Now let’s go ahead and implement the request. We’ll use the request-promise library to build a request to the FAA service mentioned earlier. Add the following to faa_data_helper.js that you created in the project's root directory:'
'use strict'; var _ = require('lodash'); var rp = require('request-promise'); var ENDPOINT = 'http://services.faa.gov/airport/status/'; function FAADataHelper() { } FAADataHelper.prototype.requestAirportStatus = function(airportCode) { return this.getAirportStatus(airportCode).then( function(response) { console.log('success - received airport info for ' + airportCode); return response.body; } ); }; FAADataHelper.prototype.getAirportStatus = function(airportCode) { var options = { method: 'GET', uri: ENDPOINT + airportCode, resolveWithFullResponse: true, json: true }; return rp(options); }; module.exports = FAADataHelper;
The test should pass now:
FAADataHelper #getAirportStatus with a valid airport code success - received airport info for SFO ✓ returns airport code (180ms)
Let’s also add a test for the eventuality that an IATA airport code isn’t one the FAA service knows about or is responding with a non-200 status code. If the server is given a faulty IATA code, it appears to return a 404 status code. Our request library interprets this as an error.
Add the following test to within the describe('#getAirportStatus', function() { part of the test_faa_data_helper.js file. We will assert that an invalid IATA airport code returns an error:
PUNKYBREWSTER is not a valid airport code ... context('with an invalid airport code', function() { it('returns invalid airport code', function() { airport_code = 'PUNKYBREWSTER'; return expect(subject.requestAirportStatus(airport_code)).to.be.rejectedWith(Error); }); }); ...
Using npm test, we find that our test should now pass:
FAADataHelper #getAirportStatus with an invalid airport code ✓ returns invalid airport code (295ms) with a valid airport code success - received airport info for SFO ✓ returns airport code (301ms)
Now that we’ve proven our request works as expected, let’s consider the response Alexa should give to a user with this data. The data we receive indicates if there’s a flight delay—or not—so let’s have our helper give Alexa either, depending on the response from the FAA’s server:
"There is currently no delay at Hartsfield-Jackson Atlanta International. The current weather conditions are Light Snow, 36.0 F (2.2 C) and wind Northeast at 9.2mph."
or
"There is currently a delay for Hartsfield-Jackson Atlanta International. The average delay time is 57 minutes. Delay is because of the following: AIRLINE REQUESTED DUE TO DE-ICING AT AIRPORT / DAL AND DAL SUBS ONLY. The current weather conditions are Light Snow, 36.0 F (2.2 C) and wind Northeast at 9.2mph."
Note: Alexa will read glyphs, so you'll need to test the output in the voice simulator to make sure that everything is working as it should.
Let’s write a test for a helper method to build these strings. We’ll have two tests, one for a case where there isn’t a delay, and one where there is. I grabbed the following JSON for our test harness from the service, but feel free to get your own.
Add this to test/test_faa_data_helper.js within the describe('FAADataHelper', function():
describe('#formatAirportStatus', function() { var status = { 'delay': 'true', 'name': 'Hartsfield-Jackson Atlanta International', 'ICAO': 'KATL', 'city': 'Atlanta', 'weather': { 'visibility': 5.00, 'weather': 'Light Snow', 'meta': { 'credit': 'NOAA\'s National Weather Service', 'updated': '3:54 PM Local', 'url': 'http://weather.gov/' }, 'temp': '36.0 F (2.2 C)', 'wind': 'Northeast at 9.2mph' }, 'status': { 'reason': 'AIRLINE REQUESTED DUE TO DE-ICING AT AIRPORT / DAL AND DAL SUBS ONLY', 'closureBegin': '', 'endTime': '', 'minDelay': '', 'avgDelay': '57 minutes', 'maxDelay': '', 'closureEnd': '', 'trend': '', 'type': 'Ground Delay' } }; context('with a status containing no delay', function() { it('formats the status as expected', function() { status.delay = 'false'; expect(subject.formatAirportStatus(status)).to.eq('There is currently no delay at Hartsfield-Jackson Atlanta International. The current weather conditions are Light Snow, 36.0 F (2.2 C) and wind Northeast at 9.2mph.'); }); }); context('with a status containing a delay', function() { it('formats the status as expected', function() { status.delay = 'true'; expect(subject.formatAirportStatus(status)).to.eq( 'There is currently a delay for Hartsfield-Jackson Atlanta International. The average delay time is 57 minutes. Delay is because of the following: AIRLINE REQUESTED DUE TO DE-ICING AT AIRPORT / DAL AND DAL SUBS ONLY. The current weather conditions are Light Snow, 36.0 F (2.2 C) and wind Northeast at 9.2mph.' ); }); }); });
Keep in mind that we are bouncing between adding to FAADataHelper and its test as we go—we’re TDDing it. For the sake of this article, we’re skipping ahead several “ping-pongs” between test file and implementation.
To make these tests pass, we’ll implement a formatAirportStatus(status) method that accepts the response from the FAA server and turns it into a sentence matching the expected above.
Let’s use the _.template() feature available in lodash to simplify the task of generating the strings Alexa will respond with. Add the following method to your FAADataHelper class:
FAADataHelper.prototype.formatAirportStatus = function(airportStatus) { var weather = _.template('The current weather conditions are ${weather}, ${temp} and wind ${wind}.')({ weather: airportStatus.weather.weather, temp: airportStatus.weather.temp, wind: airportStatus.weather.wind }); if (airportStatus.delay === 'true') { var template = _.template('There is currently a delay for ${airport}. ' + 'The average delay time is ${delay_time}. ' + 'Delay is because of the following: ${delay_reason}. ${weather}'); return template({ airport: airportStatus.name, delay_time: airportStatus.status.avgDelay, delay_reason: airportStatus.status.reason, weather: weather }); } else { // no delay return _.template('There is currently no delay at ${airport}. ${weather}')({ airport: airportStatus.name, weather: weather }); } };
Now, run the test file. You should see the following:
FAADataHelper #getAirportStatus with an invalid airport code ✓ returns invalid airport code (295ms) with a valid airport code success - received airport info for SFO ✓ returns airport code (301ms) #formatAirportStatus with a status containing no delay ✓ formats the status as expected with a status containing a delay ✓ formats the status as expected 4 passing (608ms)
Excellent! We’re green.
Now our FAADataHelper is ready to be hooked up to a skill, which we will build in the next post. We’ll also go over how we can host the skill locally and mock the requests from Amazon Echo, enabling rapid feedback on any bugs.
Been reading along without setting up your environment? Check out the source code and get started!
-Dave (@TheDaveDev)
Click here to go on to part 2.