Alexa Skill development and testing with Java

Alexa Skill development and testing with Java

As the mayority of the Alexa Skill developer tutorials are focusing on Node or Python, I would like to highlight the Java way and point out some things that I missed in the official trainings and some things that I would personally have solved differently.

For demonstration purposes, I wrote a simple fun application which finds crew members on the enterprise spaceship. A user could ask Alexa questions like “Where is captain Picard” or “Ask Enterprise where Captain Picard is” - so this application makes perfectly no sense, but it demonstrates everything a developer has to know to implement own basic skills.

The Speechlet interface

Providing an Alexa-enabled applikation requires the developer to provide an implementation to the Speechlet interface, which is:

public interface Speechlet {
    public void onSessionStarted(SessionStartedRequest ssr, Session sn) throws SpeechletException;
    public SpeechletResponse onLaunch(LaunchRequest lr, Session sn) throws SpeechletException;
    public SpeechletResponse onIntent(IntentRequest ir, Session sn) throws SpeechletException;
    public void onSessionEnded(SessionEndedRequest ser, Session sn) throws SpeechletException;
}

The functions are quite straightforward - the session related functions handle init and cleanup work for the task of instanciating or terminating a session, which in the Alexa domian the the lifetime of a coversation with the user. OnIntent gets invoked on any voice interaction which the Alexa backend is able to map to an intent based on the predefined utterances schema.

Lets take our nonsense Enterprise crew resolver:

**
 * Handler for Alexa Events on Enterprise Demo Skill
 *
 * @author drs
 */
public class EnterpriseSpeechlet implements Speechlet {

    @Autowired
    private final ICrewPositionResolver crewPositionResolver;

    public EnterpriseSpeechlet() {        
    }

    @Override
    public void onSessionStarted(SessionStartedRequest ssr, Session sn) throws SpeechletException {

    }

    @Override
    public SpeechletResponse onLaunch(LaunchRequest lr, Session sn) throws SpeechletException {
        String welcomeMessage = "<speak>Welches Crewmitglied suchst Du?</speak>";
        String welcomeReprompt = "<speak>Suchst Du wirklich niemanden?</speak>";

        // Create the ssml text output.        
        SsmlOutputSpeech speech = new SsmlOutputSpeech();
        speech.setSsml(welcomeMessage);

        // Create reprompt
        SsmlOutputSpeech repromptSpeech = new SsmlOutputSpeech();
        repromptSpeech.setSsml(welcomeReprompt);
        Reprompt reprompt = new Reprompt();
        reprompt.setOutputSpeech(repromptSpeech);

        return SpeechletResponse.newAskResponse(speech, reprompt);
    }

    @Override
    public SpeechletResponse onIntent(IntentRequest request, Session sn) throws SpeechletException {
        SpeechletResponse result = null;
        Intent intent = request.getIntent();
        String intentName = (intent != null) ? intent.getName() : null;

        switch (intentName) {
            case "WhereIsIntent":
                result = getLocationStatement(intent);
        }

        return result;
    }

    @Override
    public void onSessionEnded(SessionEndedRequest ser, Session sn) throws SpeechletException {

    }

    private SpeechletResponse getLocationStatement(Intent intent) {
        String crewMemberName = Objects.requireNonNull(
                intent.getSlot("person").getValue(),
                "");

        SsmlOutputSpeech locationSpeech = new SsmlOutputSpeech();
        locationSpeech.setSsml(String.format("<speech>%s</speech>",
                crewPositionResolver
                        .resolveCrewMember(crewMemberName)
                        .orElse("Unbekanntes Crewmitglied")));

        return SpeechletResponse.newTellResponse(locationSpeech);
    }

}

As we do not have access to the board computer and there is no Amazon Alexa service for spaceships or even a region outside the the earth athmosphere yet, we inject a mock resolver for testing purposes.

public class ConstantCrewPositionResolver implements ICrewPositionResolver {

    private final Map<String, String> crewPositions;

    public ConstantCrewPositionResolver() {
        this.crewPositions = ImmutableMap.of(
                "picard", "Captain Picard befindet sich nicht an Bord dieses Schiffes.",
                "worf", "Worf befindet sich auf Holodeck 3.",
                "chuck norris", "Chuck Norris ist draußen und schiebt.",
                "data", "Data ist in Leutnant Yars Quartier.",                
                "riker",  "Riker ist auf 10 vorne."
        );
    }

    @Override
    public Optional<String> resolveCrewMember(String crewMember) {
        return Optional.ofNullable(
                crewPositions.get(crewMember.toLowerCase()
                ));
    }            

}

Mocking an Alexa call for jUnit

Testing the data providers behind the Alexa API behaves as good as everyday testing, but mocking an Alexa request does not appear to be in the primary featureset of the Alexa API, which means that we have to completely mock the request before passing it to our handler.

Fortunately, Amazon used a library related to immutables for their API, so it is possible to compose a request which would resemble a search request for Captain Picard as following:

@Test
  public void askForPicard_shouldReturnExpectedMessage() throws SpeechletException {
    EnterpriseSpeechlet subject = new EnterpriseSpeechlet();

    // Compose a request as expected from the Alexa Service
    IntentRequest mockPicard = IntentRequest.builder()
    .withIntent(
      Intent.builder()
        .withName("WhereIsIntent")
        .withSlots(ImmutableMap.of("person",
            Slot.builder()
              .withName("person")
              .withValue("Picard")
              .build()))
        .build())
    .withRequestId("mock-picard")
    .build();

    SpeechletResponse response = subject.onIntent(mockPicard, null);        
    System.out.println("resp:" + response);
  }

What I would like to be changed in the Java API

Builders all the way

Builders are good. Please use them on the response types, aswell. For example, the code below feels very, very 90s:

SsmlOutputSpeech repromptSpeech = new SsmlOutputSpeech();
repromptSpeech.setSsml(welcomeReprompt);
Reprompt reprompt = new Reprompt();
reprompt.setOutputSpeech(repromptSpeech);

What I would like to have written without providing own facades is:

Reprompt reprompt = Reprompt.fromSsml(welcomeReprompt));

Easy testing

Mocking a request for semi-end to end testing like in the example above works, but it is not really comfortable. I would appreciate a function which exports the request to a JSON file, together with a corresponting input function. This would make it easy to mock the request without using the builders directly.

__

Recent Posts

codefreeze 2024
Fuzz Testing in Golang
Hyères
Gran Canaria
Solingen
Norderney
Scotland
Træfik
Copenhagen | København
Tenerife
Etretat
Lanzarote