Skip to content

Latest commit

 

History

History
377 lines (258 loc) · 11.4 KB

unit-testing.md

File metadata and controls

377 lines (258 loc) · 11.4 KB

Unit Testing for Voice Apps

To make sure your Alexa Skills and Google Actions are robust, we highly recommend testing. Learn how to implement unit tests for your voice apps built with Jovo.

Introduction to Unit Testing

Unit Testing is a testing method that allows you to make sure individual units of software work as expected. This way you don't have to manually test every potential interaction of your voice app after any change you do to the code, which not only saves a lot of time, but also gives you some well deserved peace of mind.

The Jovo TestSuite allows you to create unit tests for your Alexa Skills and Google Actions that can either test certain individual features, or even full conversational sequences.

Getting Started with the Jovo TestSuite

Use the Jovo Unit Testing Template to get started with some first tests.

Here's everything you need to know to get started:

Install Jest

Since Jovo v2, every new Jovo project comes with Jest as dev dependency and sample tests.

The Jovo TestSuite builds on top of Jest, a popular Javascript testing framework.

You can add Jest as dev dependencies like this:

$ npm install jest --save-dev

Create Test File

You can find an example test project here.

Unit tests are usually located in a test folder of your Jovo project. Naming conventions are <name>.test.js.

This is how a sample sample.test.js file with a single test for both Amazon Alexa and Google Assistant could look like:

// @language=javascript

'use strict';

const { App } = require('jovo-framework');
const { GoogleAssistant } = require('jovo-platform-googleassistant');
const { Alexa } = require('jovo-platform-alexa');

for (const p of [new Alexa(), new GoogleAssistant()]) {
    const testSuite = p.makeTestSuite();

    describe(`PLATFORM: ${p.constructor.name} INTENTS` , () => {
        test('should return a welcome message and ask for the name at "LAUNCH"', async () => {
            const conversation = testSuite.conversation();

            const launchRequest = await testSuite.requestBuilder.launch();
            const responseLaunchRequest = await conversation.send(launchRequest);

            expect(
                responseLaunchRequest.isAsk('Hello World! What\'s your name?', 'Please tell me your name.')
            ).toBe(true);

        });
    });
}

// @language=typescript

import { App } from 'jovo-framework';
import { GoogleAssistant } from 'jovo-platform-googleassistant';
import { Alexa } from 'jovo-platform-alexa';

for (const p of [new Alexa(), new GoogleAssistant()]) {
    const testSuite = p.makeTestSuite();

    describe(`PLATFORM: ${p.constructor.name} INTENTS` , () => {
        test('should return a welcome message and ask for the name at "LAUNCH"', async () => {
            const conversation = testSuite.conversation();

            const launchRequest = await testSuite.requestBuilder.launch();
            const responseLaunchRequest = await conversation.send(launchRequest);

            expect(
                responseLaunchRequest.isAsk('Hello World! What\'s your name?', 'Please tell me your name.')
            ).toBe(true);

        });
    });
}

Run Test Script

After you've defined some first tests, add the following script to your package.json:

"scripts": {
    "test": "jest"
},

This way, you can run the tests with npm test. Don't forget to first start the Jovo webhook:

// @language=javascript

# Run the development server
$ jovo run

# Open a new tab (e.g. cmd + t), run the script
$ npm test

// @language=typescript

# Run compile
$ npm run tsc

# Run the development server
$ jovo run

# Open a new tab (e.g. cmd + t), run the script
$ npm test

Basic Concepts

Tests can be run for each platform:

for (const p of [new Alexa(), new GoogleAssistant()]) {
    // Initialize TestSuite for each platform
    const testSuite = p.makeTestSuite();

    // Add test groups here
}

After initializing the TestSuite, you can add test groups like this:

describe(`GROUP` , () => {
    test('should...', async () => {
        // Add test content here
    });

    // More tests
});

Each test contains the following elements:

Conversation

Each test starts with a conversation:

// Initialize Conversation
const conversation = testSuite.conversation();

Conversation Configuration

You can also add certain configurations to the constructor of your conversation object.

// Initialize Conversation
const conversationConfig = {
    // Overwrite default configurations here
};
const conversation = testSuite.conversation(conversationConfig);

// Example: Use different PORT for the Jovo Webhook
const conversationConfig = {
    httpOptions: { 
        port: PORT,
    },
};
const conversation = testSuite.conversation(conversationConfig);

// Example in short
const conversation = testSuite.conversation({httpOptions: { port: PORT }});

Here is a list of the default configurations of the conversation object:

// Default Config

config: ConversationConfig = {
    userId: randomUserId(),
    locale: 'en-US',
    defaultDbDirectory: './db/tests/',
    httpOptions: {
        host: 'localhost',
        port: 3000,
        path: '/webhook',
        method: 'POST',
        headers: {
            'accept': 'application/json',
            'content-type': 'application/json',
            'jovo-test': 'true'
        },
    },
};

Request

Request Builder

You can use the request builder of the testSuite to build a request. The following request types are supported:

  • launch
  • intent
// Example: Create a launch request
const launchRequest = await testSuite.requestBuilder.launch();

// Example: Create an HelloWorldIntent request
const helloWorldIntentRequest = await testSuite.requestBuilder.intent('HelloWorldIntent');

// Example Create a MyNameIsIntent request with an input
const myNameIsIntentRequest = await testSuite.requestBuilder.intent('MyNameIsIntent', { name: 'Joe' });

After creating a request with the request builder, you can modify with several helper methods:

// Create an intent request
const request = await testSuite.requestBuilder.intent();

// Example: Update the user ID (default: each conversation gets a random user ID)
request.setUserId('<some-user-id>');

// Example: Set "new session" to true (default: false)
request.setNewSession(true);

Find out more about the helper methods for the Jovo $request object here.

Response

The previously created requests can be sent to the conversation. This will then return a response:

const response = await conversation.send(request);

// Example: Send previously created launchRequest to conversation 
const responseToLaunchRequest = await conversation.send(launchRequest);

// Example: Send previously created helloWorldIntentRequest to conversation
const responseToHelloWorldIntentRequest = await conversation.send(helloWorldIntentRequest);

You can then used these responses to compare them to expected results (see section Check below):

// Example: Returns true if the response matches this.tell('Hello World!')
response.isTell('Hello World')

Find out more about the helper methods for the Jovo $response object here.

Check

Learn more about Jest Expect here.

expect(value).toBe(value);

// Example: Launch Request should return ask
expect(
    responseToLaunchRequest.isAsk('Hello World! What\'s your name?', 'Please tell me your name.')
        ).toBe(true);

// Example: Launch Request should return right speech
expect(
    responseToLaunchRequest.getSpeech('Hello World! What\'s your name?')
        ).toBe(true);

Advanced Concepts

For more thorough testing, you can use the following advanced concepts:

i18n Keys

When you're using the Jovo i18n or CMS integrations, you might not want to make checks with strings like 'Hello World! What\'s your name?' as in the examples above, as the i18n content could change, use different variations, and languages (locales).

The solution for this is to configure the conversation in a way that the CMS only returns the i18n keys instead of strings (values), which can later be checked against expected keys.

You can do this by setting the locale of the conversation to a random string, for example keys-only, in the conversation configuration:

const conversation = testSuite.conversation({ locale: 'keys-only' });

For example, if you used this.t('key') in your app logic and now set the conversation locale to keys-only, the getSpeech() method of the unit test would (if correct) return only key instead of the string (value) stored in the CMS.

You can then use the Jest toMatch method to match it to an expected key:

expect(launchResponse.getSpeech())
    .toMatch(
            'key'
            );

If your output uses several chained keys (for example by using the SpeechBuild addT method), you can add them together like this:

expect(launchResponse.getSpeech())
    .toMatch(
            'keyOne ' +
            'keyTwo'
            );

User Data

You can use the conversation to store and retrieve user specific data as well as you would using the Jovo User object:

// Store user data
conversation.$user.$data.key = value;

// Retrieve user data
let value = conversation.$user.$data.key;

As unit tests are run locally, this will save the data into the default FileDB database.

This also allows you to access User Meta Data:

// Get the full Meta Data object
conversation.$user.$metaData

You can also delete the database for this user with the following method:

await conversation.clearDb();