Test Amazon Alexa Skills Offline With Mocha And Chai For Node.js

Datetime:2016-08-22 23:03:26          Topic: Node.js           Share

By now you’re probably aware that I’m all about Amazon Alexa skills since I’m a proud owner of an Amazon Echo.  I had released a Alexa skill calledBART Control and published a guide on creating a simple skill with Node.js and Lambda .  If you went through my Node.js and Lambda guide you probably found it pretty painful to test the skill you were working on.  The constant building and uploading to Lambda could easily get out of control.  What if I told you there was a much simpler way that could save you a ton of time?

We’re going to take a look at adding test cases for testing an Alexa skill offline without ever having to upload the skill to Lambda .

Before going forward it is best to note that we’ll be working off thesimple example we saw previously.  If you haven’t gone through the tutorial, I recommend you do before continuing.  However, it is ultimately up to you.  Also note that much of the material that comes next was heavily inspired by other blogs that I read on the internet.  I’d like to think I’m pulling the pieces together.  The various information resources will be given fair credit throughout this article.

To test my AWS Lambda code locally, I’ve been using a Node.js package called aws-lambda-mock-context created by Sam Verschueren .  This package is in combination with the very popular Mocha and Chai testing tools.  You can easily switch these out with Jasmine if you’d like.

At this point your project should look like the following:

  • package.json
  • src
    • data.js
    • index.js

We’re going to create a directory at the root of the project called test as it is a requirement for Mocha.  After you’ve created that directory, let’s install both Chai and Mocha:

npminstallmochachai --save-dev

With Mocha and Chai installed we still need to install the aws-lambda-mock-context package.  With your Command Prompt (Windows) or Terminal (Mac and Linux), execute the following:

npminstallaws-lambda-mock-context --save-dev

Per the official aws-lambda-mock-context documentation , there isn’t a whole lot to the package.  For example, this is actual usage of it with Node.js:

const context = require('aws-lambda-mock-context');
 
const ctx = context();
 
index.handler({hello: 'world'}, ctx);
 
ctx.Promise.then(() => {
    //=> succeed() called
}).catch(err => {
    //=> fail() called
});

The above code isn’t really the entire story though.  For example, what is index or what is  {hello: "world"} ?  Let’s dig a bit deeper to figure this stuff out.

The index variable actually needs to be declared.  In our example it actually links to the  index.js file, the core part of our Lambda code.  So to be clear, it would look like this:

var index = require('../src/index');

The above line assumes we’re inside a test/aboutintent.js file.  Now how about the object that is passed into this handler function?  Well, let’s try to remember what exactly  index.handler is:

exports.handler = function (event, context) {
    var polyglot = new Polyglot();
    polyglot.execute(event, context);
};

The object in this case is the Lambda event.

If you made it to the publishing stages of your Alexa skill, you’d have come across a testing stage that generates a Lambda event.  You can go ahead and copy that event to your clipboard.  You can also get your hands on a Lambda event via the AWS dashboard.  If neither are convenient for you, then go ahead and use my Lambda event:

{
    "session": {
        "sessionId": "SessionId.154291c5-a13f-4e7a-ab5a-24234adf",
        "application": {
            "applicationId": "amzn1.echo-sdk-ams.app.APP_ID"
    },
    "attributes": {},
    "user": {
        "userId": null
    },
    "new": true
    },
    "request": {
        "type": "IntentRequest",
        "requestId": "EdwRequestId.474c15c8-14d2-4a77-a4ce-154291c5",
        "timestamp": "2016-07-05T22:02:01Z",
        "intent": {
            "name": "AboutIntent",
            "slots": { }
        },
        "locale": "en-US"
    },
    "version": "1.0"
}

That particular Lambda event would trigger the AboutIntent of our Alexa skill.

So how do we bring this all together for testing?  Assuming you’ve created a file called test/aboutintent.js , open it and include the following JavaScript code:

var expect = require('chai').expect;
var index = require('../src/index');
 
const context = require('aws-lambda-mock-context');
const ctx = context();
 
describe("Testing a session with the AboutIntent", function() {
    var speechResponse = null
    var speechError = null
 
    before(function(done){
        index.handler({
            "session": {
                "sessionId": "SessionId.154291c5-a13f-4e7a-ab5a-2342534adfeba",
                "application": {
                    "applicationId": "amzn1.echo-sdk-ams.app.APP_ID"
            },
            "attributes": {},
            "user": {
                "userId": null
            },
            "new": true
            },
            "request": {
                "type": "IntentRequest",
                "requestId": "EdwRequestId.474c15c8-14d2-4a77-a4ce-154291c5",
                "timestamp": "2016-07-05T22:02:01Z",
                "intent": {
                    "name": "AboutIntent",
                    "slots": { }
                },
                "locale": "en-US"
            },
            "version": "1.0"
        }, ctx)
 
        ctx.Promise
            .then(resp => { speechResponse = resp; done(); })
            .catch(err => { speechError = err; done(); })
    })
 
    describe("The response is structurally correct for Alexa Speech Services", function() {
        it('should not have errored',function() {
            expect(speechError).to.be.null
        })
 
        it('should have a version', function() {
            expect(speechResponse.version).not.to.be.null
        })
 
        it('should have a speechlet response', function() {
            expect(speechResponse.response).not.to.be.null
        })
 
        it("should have a spoken response", () => {
            expect(speechResponse.response.outputSpeech).not.to.be.null
        })
 
        it("should end the alexa session", function() {
            expect(speechResponse.response.shouldEndSession).not.to.be.null
            expect(speechResponse.response.shouldEndSession).to.be.true
        })
    })
})

There is a lot of code above so we’re going to break it down.  Before we do that, I’d like to say that I found most of the above code from Casey MacPherson’s developer blog Codedad .  While his blog put me on the right track, I did make a few changes.

The first thing you’ll notice in the above test is that we’re including Chai and getting the context.  Nothing special yet.

Next we’re describing what will be a series of test scenarios.

Because our Lambda call is asynchronous we need to execute it in the before() hook in Mocha.  We’re doing this so we can wait until it completes before moving on.  Notice the code below:

ctx.Promise
    .then(resp => { speechResponse = resp; done(); })
    .catch(err => { speechError = err; done(); })

Unless we call done() , the other test cases will wait.  This allows us to collect the mock Lambda response first, whether it succeeds or fails.

Every test that comes next will check bits and pieces from our response.  For example:

it("should end the alexa session", function() {
    expect(speechResponse.response.shouldEndSession).not.to.be.null
    expect(speechResponse.response.shouldEndSession).to.be.true
})

We know our AboutIntent should not wait for user feedback, thus the session will end immediately after Alexa responds.  If it doesn’t, we know there is a problem.

Your tests can be much more complex than the few that I listed.  Maybe you want to validate specifics on the response data, like if Alexa spoke the correct text.  You absolutely can do this.

Just for closure, here is the test I used for LanguageIntent which is the second intent:

var expect = require('chai').expect;
var index = require('../src/index');
 
const context = require('aws-lambda-mock-context');
const ctx = context();
 
describe("Testing a session with the LanguageIntent", function() {
    var speechResponse = null
    var speechError = null
 
    before(function(done){
        index.handler({
            "session": {
                "sessionId": "SessionId.154291c5-a13f-4e7a-ab5a-707ca12501a8",
                "application": {
                    "applicationId": "amzn1.echo-sdk-ams.app.APP_ID"
            },
            "attributes": {},
            "user": {
                "userId": null
            },
            "new": true
            },
            "request": {
                "type": "IntentRequest",
                "requestId": "EdwRequestId.474c15c8-14d2-4a77-a4ce-11728a114af7",
                "timestamp": "2016-07-05T22:02:01Z",
                "intent": {
                    "name": "LanguageIntent",
                    "slots": {
                        "Language": {
                            "name": "Language",
                            "value": "ionic"
                        }
                    }
                },
                "locale": "en-US"
            },
            "version": "1.0"
        }, ctx)
 
        ctx.Promise
            .then(resp => { speechResponse = resp; done(); })
            .catch(err => { speechError = err; done(); })
    })
 
    describe("The response is structurally correct for Alexa Speech Services", function() {
        it('should not have errored',function() {
            expect(speechError).to.be.null
        })
 
        it('should have a version', function() {
            expect(speechResponse.version).not.to.be.null
        })
 
        it('should have a speechlet response', function() {
            expect(speechResponse.response).not.to.be.null
        })
 
        it('should have session attributes', function() {
            expect(speechResponse.response.sessionAttributes).not.to.be.null
        })
 
        it("should have a spoken response", () => {
            expect(speechResponse.response.outputSpeech).not.to.be.null
        })
 
        it("should end the alexa session", function() {
            expect(speechResponse.response.shouldEndSession).not.to.be.null
            expect(speechResponse.response.shouldEndSession).to.be.true
        })
    })
})

The above test code was found in my test/languageintent.js source file.

Executing the above tests are very easy.  Using the command line you’ll want to run mocha which will perform any tests found in the  test directory of your project.  You’ll get a pass or fail for every test checkpoint for every test file.

Conclusion

Being able to test your Alexa skill offline on your local machine is huge.  Of course this is only if you’re using Node.js and AWS Lambda , but if you are, it will save you a ton of development time.  In this guide we saw how to add tests to theprevious example skill project that we worked on.  The package by Sam Verschueren and the write-up by Casey MacPherson put me on a solid track.  The goal here was to fill any gaps in the process.





About List