DEV Community

Cover image for Alexa Skills Testing
Tomer Figenblat
Tomer Figenblat

Posted on • Updated on

 

Alexa Skills Testing

Test your Java Alexa skills with real requests

Writing an Alexa skill in Java is pretty simple using the Alexa Skills Kit SDK for Java. 😃

Testing your code, however, can be quite troublesome.

You can, and should, write the required unit test cases to verify the integrity of your code.
But...
If you want to test your overall skill interaction...
Well, this can be tricky and messy to accomplish using your standard testing tools. 😵

There are a couple of solutions for testing skills, but the ones I found (for Java), require the skill to be deployed/hosted and accessible for the test cases to run.

I was looking for a tool that can help me write automated test cases for my Alexa skills, a tool that can be used in my unit/integration tests to fire real-life JSON requests at my code and verify the multi-turn interactions with my skill, without having to deploy or host it first.

When I didn't find such a tool, I decided to build one.
My alexa-skills-tester 😀

The sources for this post can be found here.

To use the alexa-skills-tester, just add the following to your project:
(replace VERSION with the current release).

<dependency>
  <groupId>info.tomfi.alexa</groupId>
  <artifactId>alexa-skills-tester</artifactId>
  <version>VERSION</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode

Example usage

Using a basic Nice to meet you custom skill:
When launching the skill, it asks for the user name and then replies with a Nice to meet you #name prompt, ending the session.

Let's take a look at the code for the skill.

A launch request handler

public final class LaunchRequestHandlerImpl implements LaunchRequestHandler {
  @Override
  public boolean canHandle(final HandlerInput input, final LaunchRequest request) {
    return true;
  }

  @Override
  public Optional<Response> handle(final HandlerInput input, final LaunchRequest request) {
    return input.getResponseBuilder()
        .withSpeech("What is your name?")
        .withReprompt("Please tell me your name.")
        .withShouldEndSession(false)
        .build();
  }
}
Enter fullscreen mode Exit fullscreen mode

An intent request handler handling MyNameIntent with a nameSlot

public final class MyNameIntentRequestHandler implements IntentRequestHandler {
  @Override
  public boolean canHandle(final HandlerInput input, final IntentRequest request) {
    return request.getIntent().getName().equals("MyNameIntent");
  }

  @Override
  public Optional<Response> handle(final HandlerInput input, final IntentRequest request) {
    var name = request.getIntent().getSlots().get("nameSlot").getValue();
    return input.getResponseBuilder()
        .withSpeech(String.format("Nice to meet you %s!", name))
        .withShouldEndSession(true)
        .build();
  }
}
Enter fullscreen mode Exit fullscreen mode

A session ended request handler

public final class SessionEndedRequestHandlerImpl implements SessionEndedRequestHandler {
  @Override
  public boolean canHandle(final HandlerInput input, final SessionEndedRequest request) {
    return true;
  }

  @Override
  public Optional<Response> handle(final HandlerInput input, final SessionEndedRequest request) {
    return Optional.empty();
  }
}
Enter fullscreen mode Exit fullscreen mode

A utility class for constructing the skill

class NiceToMeetYouSkill {
  public static Skill getSkill() {
    return Skills.standard()
        .addRequestHandler(new LaunchRequestHandlerImpl())
        .addRequestHandler(new MyNameIntentRequestHandler())
        .addRequestHandler(new SessionEndedRequestHandlerImpl())
        .build();
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, let's test the skill

You should get familiarized with Alexa's Request and Response JSON Reference. This is of course should be considered common knowledge for skill developers.

Let's create a couple of fake request JSON files to use in our tests.

launch_request.json

{
  "version": "1.0",
  "session": {
    "new": true
  },
  "context": {
    "System": {}
  },
  "request": {
    "type": "LaunchRequest",
    "requestId": "amzn1.echo-api.request.fake-request-id",
    "timestamp": "2021-02-11T15:30:00Z",
    "locale": "en-US"
  }
}

Enter fullscreen mode Exit fullscreen mode

my_name_intent.json

{
  "version": "1.0",
  "session": {
    "new": false
  },
  "context": {
    "System": {}
  },
  "request": {
    "type": "IntentRequest",
    "requestId": "amzn1.echo-api.request.fake-request-id",
    "timestamp": "2021-02-11T15:31:00Z",
    "locale": "en-US",
    "intent": {
      "name": "MyNameIntent",
      "slots": {
        "nameSlot": {
          "name": "nameSlot",
          "value": "master"
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

session_ended.json

{
    "version": "1.0",
    "session": {
        "new": false
    },
    "context": {
        "System": {}
    },
    "request": {
        "type": "SessionEndedRequest",
        "requestId": "amzn1.echo-api.request.fake-request-id",
        "timestamp": "2021-02-11T15:32:00Z",
        "locale": "en-US"
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, let's create some test cases leveraging the above JSON files to verify the interaction with the skill.

Test cases testing the Nice to meet you skill

final class SkillInteractionTest {
  @Test
  void verify_greeting_for_my_name_intent_as_a_followup_to_a_launch_request() throws IOException {
    givenSkill(NiceToMeetYouSkill.getSkill())
        .whenRequestIs(getClass().getClassLoader().getResourceAsStream("launch_request.json").readAllBytes())
        .thenResponseShould()
            .waitForFollowup()
            .haveOutputSpeechOf("What is your name?")
            .haveRepromptSpeechOf("Please tell me your name.")
        .followingUpWith(getClass().getClassLoader().getResourceAsStream("my_name_intent.json").readAllBytes())
        .thenResponseShould()
            .haveOutputSpeechOf("Nice to meet you master!")
            .and()
            .notWaitForFollowup();
  }

  @Test
  void verify_empty_response_for_session_ended_requests() throws IOException {
    givenSkill(NiceToMeetYouSkill.getSkill())
      .whenRequestIs(getClass().getClassLoader().getResourceAsStream("launch_request.json").readAllBytes())
      .thenResponseShould()
          .waitForFollowup()
          .haveOutputSpeechOf("What is your name?")
          .haveRepromptSpeechOf("Please tell me your name.")
      .followingUpWith(getClass().getClassLoader().getResourceAsStream("session_ended.json").readAllBytes())
      .thenResponseShould().beEmpty();
  }
}
Enter fullscreen mode Exit fullscreen mode

That's it!
Hope you'll find this tool useful.

Please note, I don't develop skills regularly, and this tool was written based on my own limited requirements.
There are a lot of assertion methods and features that can be added to this tool.
Please feel free to open a feature request issue, describing the feature you would like to see, or push a pull request if you want to contribute the feature yourself. 😎

You can check out the code for this part of the tutorial in Github.

👋 See you in the next tutorial 👋

Top comments (1)

Collapse
 
kini profile image
Joaquín Engelmo (a.k.a. Kini)

Great post @tomerfi :)

I will give a try in my skills!

Thanks

An Animated Guide to Node.js Event Loop

Node.js doesn’t stop from running other operations because of Libuv, a C++ library responsible for the event loop and asynchronously handling tasks such as network requests, DNS resolution, file system operations, data encryption, etc.

What happens under the hood when Node.js works on tasks such as database queries? We will explore it by following this piece of code step by step.