question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Adding the ability to perform unit tests easily. Here’s how I have tests setup now using @bespoken tools and ava. This isn’t the exact code behind it but an overview of it and how I’m currently using it.

SkillMock API

All the credit goes to @bespoken, I just converted what they built to be promised based so it was easier for me to work with.

const skill = new SkillMock(/* app id */)

// Starts and stops Alexa and the lambda server
skill.start(), skill.stop()

// launches the voice assistant (aka 'Alexa, ', 'Hey Google')
skill.launched()

// ends the session
skill.sessionEnded(reason)

// run intent by name and pass slots into them
skill.intended(intent, slots)

// run intents by passing in the phrases they're associated with
skill.spoken(utterance)

// set the access token for the app
skill.setAccessToken(token)

// This one isn't apart of bespoken tools. It kicks off tests ability to test easily via the response 
// that's stored when `skill.launched`, `skill.intended`, `skill.spoken`, or `skill.sessionEnded`. 
// This just returns the class that I built to help with testing
skill.test(assertion)

Testing API
// skill.test() exposes this api
// All these methods and getter functions are chainable, and they all start from skill.test()
// here's a list of assertion methods that can be used in each context
.is(str) // Assert that the value is the same as the output speech
.truthy() // Assert that the response value is truthy
.falsy() // Assert that the response value is falsy
.not(str) // Assert that the value is not the same as the output speech
.matches(regex), .match(regex) // Assert that the output speech matches regex
.notMatches(regex), .notMatch(regex) // Assert that the output speech doesn't match regex
.startsWith(str) // Assert that the output speech starts with the string
.notStartsWith(str) // Assert that the output speech doesn't start with the string
.endsWith(str) // Assert that the output speech ends with the string
.notEndsWith(str) // Assert that the output speech doesn't end with the string
.includes(str), .contains(str) // Assert that the output speech contains string
.notIncludes(str), .notContains(str) // Assert that the output speech doesn't contain string
.ended() // Assertion to ensure the session has ended
.notEnded() // Assertion to ensure the session hasn't ended

// These getter methods will change the style. So if you want to test against ssml or plain text you have the option
.ssml // changes the context style to ssml (default)
.plain // changes the context style to plain

// These getter methods change the context to test different aspects of the voice app
// all these work with the methods above
.response // changes the context to response (default)
.reprompt // changes the context to reprompt

.sessionAttributes, .attr, .attributes // changes the context to attributes
  // on top of the default assertions here are some attr specific assertions
  .attr.keys(...keys), .attr.key(...keys) // Assert that the keys exist
  .attr.notKeys(...keys), .attr.notKey(...keys) // Assert that the key doesn't exists
  .attr.is(key, expected), .attr.value(key, expected) // Assert that the key value is same as expected
  .attr.not(key, expected), .attr.notValue(key, expected) // Assert that the key value isn't the same as expected
  .attr.type(key, type) // Assert that the key's value is same as type that was passed (`[]` = 'array', `null` = 'null')
  .attr.notType(key, type) // Assert that the key's value isn't the same as the type that was passed
  .attr.truthy(key) // Assert that the key's value is truthy
  .attr.falsy(key) // Assert that the key's value is falsy

.card // changes the context to cards
  card.title // changes the context to the cards title (default)
  card.type // changes the context to the cart type
  card.image.small, card.small // changes the context to small card image
  card.image.large, card.large // changes the context to small card image
  card.text // changes the content of the card

Here’s a basic example of how it looks in a test

test('basic', async (t) => {
  const skill = new SkillMock()
  await skill.start()
  skill.setAccessToken(await mockAccessToken())

  await skill.launched() // Alexa, open [my app]
  await skill.spoken('what\'s my cashback balance')

  skill.test(t)
    .matches(/^your cashback balance is \$[0-9.]+\./) // ensure the response matches this format
    .reprompt
    .is('what else can i help you with?') // ensure the reprompt is exactly this

  await skill.stop()
})
alexa json response for basic
{
  "version": "1.0",
  "response": {
    "shouldEndSession": false,
    "outputSpeech": {
      "type": "SSML",
      "ssml": "<speak> your cashback balance is $10.20. what else can i help you with? </speak>"
    },
    "reprompt": {
      "outputSpeech": {
        "type": "SSML",
        "ssml": "<speak> what else can i help you with? </speak>"
      }
    }
  },
  "sessionAttributes": {
    "shopper_id": "112341234jlkajfasda2431asddf",
    "customer_id": "5559316",
    "distributor_id": "123412341234",
    "portal_id": 132241234,
    "language": "ENG",
    "country": "USA",
    "gender": "Male",
    "shopper_name": "John Doe",
    "STATE": "_MAIN"
  }
}

Here’s more of an advanced use case

test('advanced', async (t) => {
  const expected_prompt = 'would you like to order this product?'
  const skill = new SkillMock()
  await skill.start()
  skill.setAccessToken(await mockAccessToken())

  await skill.launched() // Alexa, open [my app]
  await skill.spoken('search for {Xbox One X}')

  skill.test(t)
    .includes('Microsoft <break time="100ms" /> Xbox One X') // ensure there's a breaktime in there after the product name. It makes it sound better
    .plain // switch to plain context
      .matches(/is \$(?:[0-9]{2,}|[1-9]+).[0-9]{2} usd/i) // ensure the price is formatted correctly on the response
      .includes(expected_prompt) // ensure the prompt is apart of the initial response
    .reprompt // switch to the reprompt context
      .is(expected_prompt) // ensure the reprompt matches prompt
    .card // switch to the card context
      .matches(/is \$(?:[0-9]{2,}|[1-9]+).[0-9]{2} usd/i) // ensure the price is formatted correctly
    .text // switch to the `card.text` context
      .notIncludes(expected_prompt) // ensure the card text doesn't include the prompt message
    .image
      .small // switch to the small image context
        .truthy() // ensure the small image exists
        .matches(/__300x300__\.jpg$/i) // ensure the small image is the 300 size
      .large // switch to the large image context
        .truthy() // ensure the large image exists
        .matches(/__600x600__\.jpg$/i) // ensure the small image is the 600 size
    .attr // switch to the attributes context
      .truthy('product') // esnure the product key exists
      .type('product', 'object') // ensure the product key is an object

  await skill.stop()
})
alexa json response for advanced
{
  "version": "1.0",
  "response": {
    "shouldEndSession": false,
    "outputSpeech": {
      "type": "SSML",
      "ssml": "<speak> Microsoft <break time=\"100ms\" /> Xbox One X - is $499.00 usd. would you like to order this product? </speak>"
    },
    "reprompt": {
      "outputSpeech": {
        "type": "SSML",
        "ssml": "<speak> would you like to order this product? </speak>"
      }
    },
    "card": {
      "type": "Standard",
      "title": "Microsoft Xbox One X is $499.00 USD.",
      "image": {
        "smallImageUrl": "https://img.shop.com/Image/240000/243400/243416/products/1588637534__300x300__.jpg",
        "largeImageUrl": "https://img.shop.com/Image/240000/243400/243416/products/1588637534__600x600__.jpg",
      },
      "text": "Games play better on Xbox One X. Experience 40% more power than any other console 6 teraflops of graphical processing power and a 4K Blu-ray player provides more immersive gaming and entertainment Play with the greatest community of gamers on..."
    }
  },
  "sessionAttributes": {
    "shopper_id": "asdasdfas31sdq14gasdasfd",
    "customer_id": "12341242994",
    "portal_id": 1234123,
    "language": "ENG",
    "country": "USA",
    "gender": "Male",
    "shopper_name": "john doe",
    "STATE": "_SEARCH_RESULTS",
    "product": {
      "id": 112312341234,
      "category_id": 12342,
      "review_count": 341,
      "pricing": "499.00",
      "is_accessory": false,
      "prod_container_id": 1123412341234
      "merchant_sku": "33414",
      "product_category_id": 12341,
      "prod_id": "1588637534",
      "category_name": "Electronic",
      "rating": 5,
      "container_text": "Microsoft Xbox One X",
      "container_text_phonetic": "Microsoft <break time=\"100ms\" /> Xbox One X",
      "discount_percentage": 0,
      "description": "Games play better on Xbox One X. Experience 40% more power than any other console 6 teraflops of graphical processing power and a 4K Blu-ray player provides more immersive gaming and entertainment Play with the greatest community of gamers on...",
      "image": {
        "small": "https://img.shop.com/Image/240000/243400/243416/products/1588637534__300x300__.jpg",
        "large": "https://img.shop.com/Image/240000/243400/243416/products/1588637534__600x600__.jpg",
      },
      "on_sale": false,
      "locale_id": 10,
      "cashback": "0.47",
      "store_name": "Walmart",
      "currency_code": "USD"
    }
  }
}

Example of how my test files are setup

Real world use case
import SkillMock, { mockAccessToken } from '../../skill-mock'
import ava from 'ava-spec'
const test = ava.group('handlers:main:cashback')

test.beforeEach(async (t) => {
  // initialize the skill (aka `bst.LambdaServer` `bst.BSTAlexa`)
  t.context.skill = new SkillMock()
  // start the skill starts the lambda and alexa servers
  await t.context.skill.start()
  // set the access token for my app. We have our own service behind the scenes that generates it for us.
  t.context.skill.setAccessToken(await mockAccessToken())
})

test.afterEach(async (t) => {
  // after each test stop the skill (aka the lambda and alexa servers)
  await t.context.skill.stop()
})


test.group('CashbackBalanceCheck', (test) => {
  const utterance = 'what is my cashback'

  test('success', async (t) => {
    const skill = t.context.skill
    await skill.launched() // launch the skill (aka `Alexa, `, `Hey Google`,)
    await skill.spoken(utterance) // pass in the utterance that's being tested 
    // since I was already using ava I just pass in their assertion lib to the test class I wrote.
    skill.test(t)
      .plain // convert the response to plain text (strip out the ssml)
      .matches(/^your cashback balance is \$[0-9.]+\./) // check to see if the response matches this regex
      .reprompt // move on to the reprompt
      .is('what else can i help you with?')  // ensure the reprompt is exactly `'what else can i help you with?'`
  })

  test('no cashback', async (t) => {
    const skill = t.context.skill
    // I had to test to what no cashback response looked like which required me use 
    // a different email address so instead of using the initial `accessToken` I set a different one
    skill.setAccessToken(await mockAccessToken('no-cashback-live@yopmail.com'))
    await skill.launched()
    await skill.spoken(utterance)
    skill.test(t)
      .plain
      .includes('you do not currently have any cashback') // the response includes this text
      .reprompt
      .is('what else can i help you with?') // the response is exactly this text
  })

  test('error', async (t) => {
    const skill = t.context.skill
    await skill.launched()
    // cause an error by setting shopper_id to be wrong
    skill.attributes.shopper_id = 'asdfasdfasdfawasdf'
    await skill.spoken(utterance)
    skill.test(t)
      .plain
      .is('something went wrong while trying to get your cashback balance. what else can i help you with?') // ensure the response is this exact text
  })
})

These don’t cover every single thing you could test but I think it’s a good starting point. I took the great work that bespoken has already done and tried to simplify it to make it a little more stream line for my use. Then I made the test interface to make my tests more readable, I modeled it after nixt which is a cli testing framework that simplifies testing cli tools. There’s other things that could definitely be added but this covered most of my use cases.

Issue Analytics

  • State:closed
  • Created 6 years ago
  • Reactions:1
  • Comments:7 (3 by maintainers)

github_iconTop GitHub Comments

1reaction
jkelviecommented, Nov 8, 2017

Hi @tjbenton - @jankoenig brought this to my attention. You mentioned converting our library to promises - we have pulled out our testing piece and also “promise-fied” it here: https://github.com/bespoken/virtual-alexa

We also made instantiation simpler - there is no need to start or stop anything if you have a lambda function.

It looks like you have added other pieces as well to the skill-mock though, for covering common expectation checking scenarios? Am I understanding it correctly?

1reaction
tjbentoncommented, Nov 7, 2017

let me know, I already have the js written. I would only need to adjust a couple things to make it work.

Another though I had last night was to possibly set up a test runner similar to testcafe and have specific platforms as plugins instead of browsers as plugins

Read more comments on GitHub >

github_iconTop Results From Across the Web

Unit Testing Tutorial – What is, Types & Test Example - Guru99
Unit Testing is a type of software testing where individual units or components of a software are tested. The purpose is to validate...
Read more >
Unit testing - Wikipedia
In computer programming, unit testing is a software testing method by which individual units of source code—sets of one or more computer program...
Read more >
What Is Unit Testing? - SmartBear
Unit Testing is the process of checking small pieces of code to deliver information early and often, speeding your testing strategies, and reducing...
Read more >
Unit Testing | Software Testing - GeeksforGeeks
Unit Testing is defined as a type of software testing where individual components of a software are tested. Unit Testing of the software...
Read more >
Unit Testing - SOFTWARE TESTING Fundamentals
UNIT TESTING, also known as COMPONENT TESTING, is a level of software testing where individual units / components of a software are tested....
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found