omohayui blog

おも‐はゆ・い【面映ゆい】[形][文]おもはゆ・し[ク]《顔を合わせるとまばゆく感じられる意》きまりが悪い。てれくさい。

E2E Testing for Google Assistant Apps

これは何か?

先日、GDG Tokyo New Year LT大会 2021 で「E2E Testing for Google Assistant Apps」というテーマでLTをさせて頂いたのですが、5分では伝えきれない点があったので、こちらの記事に詳細を書くことにしました。

会話形アクションにこそ自動テストが必要

GoogleアシスタントアプリやAlexaスキルを作ったことがある方は、経験されているかもしれませんが、 会話型アクションをテストするのってすごく大変なんですね。
ちょっとインテントのフレーズを変えただけでも、その対象のシーンまで会話を持っていかないといけないので、何度も同じことを言ったりして労力を奪われてしまいます。
そこで必要になってくるのが End to End の自動テストです。
一度テストを作ってしまえば、改修のたびに都度都度、話しかけるという手間もなくなります。

Assistant Conversation Testing Library

github.com

Actions on Google には E2Eテストがコードベースで書けるライブラリが用意されています。
これは、Actions API をラップしていて、テストスイートを定義すれば、自分で作ったアクションにクエリを送信して、返ってきたレスポンスに対して検証することができます。
また、テスト実行前に Actions API の writePreview メソッドをコールすることで、Draftから自分のプロジェクトにプレビューを作成し、そこに対してテストすることができます。つまり、リリース前にひととおりの検証ができるってことですね。

Setup

Actions API を有効にする

従来のテストライブラリ actions-on-google-testing-nodejs ではテスト対象の Action のプロジェクトとは別のプロジェクトの Actions API を使ってテストができたのですが、こちらはテスト対象のプロジェクトで Actions API を有効にする必要があります。
また、比較的最近作られた Actionプロジェクトではデフォルトで Actions API は有効になっているそうです。

  1. Google API Consoleにアクセス
  2. プロジェクトをテスト対象のものに切り替え
  3. "Actions API" を検索し、もしenabled になっていない場合は enable にする

f:id:omohayui:20210207001841p:plain:w300

Service Account Key を作成する

  1. 同じプロジェクトで、Credentials pageにアクセス
  2. "Create credentials" > "Service account" をクリック
  3. service account name に actions のテスト用だと分かるような名前をつけて Create
  4. Role に "Actions" > "Actions Admin" を指定する
  5. Service Account Key をJSONファイルでダウンロード

f:id:omohayui:20210207002923p:plain:w300

READMEには Role(権限) に project.OWNER を付けろって書いてあったけど、強すぎる。actions.ADMIN で動いたのでこちらがおすすめ → こちら PullRequest 出したら merge してもらえました!

github.com

環境変数 GOOGLE_APPLICATION_CREDENTIALS の設定

ダウンロードした JSON ファイルを環境変数GOOGLE_APPLICATION_CREDENTIALS セットします。
他のプロジェクトでは使いたくないものなので、direnv などを使って .envrc に記載しておくのがおすすめです。

export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service_account.json

ライブラリのインストール

npm install @assistant/conversation-testing --save
# 以下は必要な人は入れる
npm install @types/mocha --save
npm install mocha --save
npm install typescript --save

Test Code 作成

テストコードを書いていきます。
ここでは、今日食べるものを占ってくれる「カワウソ食べ物占い」のテストコードをサンプルにしています。

import 'mocha';
import {ActionsOnGoogleTestManager} from '@assistant/conversation-testing';

const PROJECT_ID = 'otter-fortune-telling-xxxxx';
const TRIGGER_PHRASE = 'カワウソ食べ物占いにつないで'; // 呼び出すフレーズ

const DEFAULT_LOCALE = 'ja-JP';  // 各言語の指定
const DEFAULT_SURFACE = 'PHONE'; // 各デバイスの指定

describe('Action project', function () {
  // write Preview するときは set timeout したほうが良い
  this.timeout(60000);
  let test: ActionsOnGoogleTestManager;

  before('setup test suite', async function() {
    test = new ActionsOnGoogleTestManager({ projectId: PROJECT_ID });
    await test.writePreviewFromDraft();
    test.setSuiteLocale(DEFAULT_LOCALE);
    test.setSuiteSurface(DEFAULT_SURFACE);
  });

プロジェクトIDには、テストしたいアクションのプロジェクトIDを記載し、トリガーフレーズにはアクションを呼び出すフレーズを入れます。 ロケールsurfaceでは、テストしたい言語やデバイスを指定することができます。

// おやつを占うテストパス
it('should match Snack Type, and end the conversation', async function () {
  await test.sendQuery(TRIGGER_PHRASE);

  // TestManager を使って API レスポンスをアサーションする
  test.assertSpeech('カワウソたべもの占いへようこそ! 占ってほしいのはランチ?ディナー?それともおやつ?');

  // ユーザークエリをアクションに返答する
  await test.sendQuery('おやつ');

  // 正規表現を使ってアサーションする
  test.assertSpeech(`ふむふむ。おやつか〜。\nそんな君におすすめのおやつは.*楽しみだね〜!もう一回やる?`, {isRegexp: true});

  // 会話を終了させる返答をする
  await test.sendQuery('やらない');
  test.assertSpeech('じゃあまたね!');
  // 会話が終了していることを確認する
  test.assertConversationEnded();
});

ここでは、テストスイートを定義しているのですが、お菓子を占ってもらって会話終了、という流れをテストしています。
このテストマネージャーには各種アサーションのメソッドが用意されているので、それを使ってレスポンスや、会話が終了していることを検証します。
もちろん正規表現やリストも使えるので、柔軟にテストは書いていくことができます。

// ランチを占ってから、もう一回占うテストパス
it('should match Lunch Type, and again the conversation', async function () {
  await test.sendQuery(TRIGGER_PHRASE);

  test.assertSpeech('カワウソたべもの占いへようこそ! 占ってほしいのはランチ?ディナー?それともおやつ?');

  await test.sendQuery('ランチ');
  test.assertSpeech(`ふむふむ。ランチか〜。\nそんな君におすすめのおやつは.*楽しみだね〜!もう一回やる?`, {isRegexp: true});

  // 会話を続ける返答をする
  await test.sendQuery('やる');
  // シーンAgainに遷移したことを確認する
  test.assertScene('Again');
  test.assertSpeech('占ってほしいのはランチ?ディナー?それともおやつ?');
  // 会話が終了していないことを確認する
  test.assertConversationNotEnded();
});

このテストでは、占ったあとに会話を終了せずにシーン Againに遷移することをテストしています。

Run Tests

先程作成したテストコードのファイルは、src/test/ 下に配置します。 そして、package.json に以下の script コマンドを追加しておきます。

  "scripts": {
     "test": "mocha --recursive --require ts-node/register src/test/*.ts",
   },

テストを実行します。

npm run test

出力結果

Failed した結果

f:id:omohayui:20210207155659p:plain:w600

「もう一回やる?」「やる」のあとに、シーン Again に遷移してほしいのに、会話が終了してしまい、期待値と異なるメッセージが返ってきています。 そして、コンソールには期待したレスポンスと実際に返ってきたレスポンスのDIFFが表示されます。

Pass した結果

f:id:omohayui:20210207155756p:plain:w800

レーニングフレーズを修正すると、テストが通りました。

Assertions 各種

用意されているアサーションは、assertSpeech や assertScene だけではありません。 Assertions を確認してください。

  • assertText - テキストレスポンスの検証
  • assertCard, assertImage, assertTable, etc - 各表示要素の検証
  • assertIntent - クエリにマッチしたインテントの検証
  • assertSessionParam, assertUserParam, assertHomeParam - セッションストレージなどストレージへのパラメータの検証

ただ、まだビルトインインテントトランザクションのテストは未対応となっているので、これから拡張されることを期待しています。

まとめ

  • 会話型アクションのインテグレーションテストはとても大変
    • でも自動テストがあれば改修も気軽にできる
  • Actions on Google には E2Eテスト用のライブラリが提供されている
    • Draft の状態に対して E2Eテストができるので、リリースする前の自動テストとして利用できる
    • テストモジュールにはさまざまなアサーションが用意されているので、ディスプレイに表示する要素のテストもできる
    • 現時点では、ビルトインインテントトランザクションのテストは未対応

所感:やはりこれだけの内容を5分でやるのは無理があった・・・