$30 off During Our Annual Pro Sale. View Details »

外部に依存したコードもテストで駆動する / Test-Driven Architecture - AWS Dev Day Tokyo 2018

外部に依存したコードもテストで駆動する / Test-Driven Architecture - AWS Dev Day Tokyo 2018

Oct 31, 2018 at AWS Dev Day Tokyo 2018 #AWSDevDay #AWSTDD

Takuto Wada
PRO

October 31, 2018
Tweet

More Decks by Takuto Wada

Other Decks in Programming

Transcript

  1. ֎෦ʹґଘͨ͠ίʔυ΋
    ςετͰۦಈ͢Δ
    ࿨ా୎ਓ !U@XBEB

    0DU !"84%FW%BZ5PLZP

    View Slide

  2. #AWSDevDay
    #AWSTDD
    ࡱӨ0,ʢͨͩ͠ɺγϟολʔԻΛ߇͑Ίʹʣ
    ࢿྉө૾ެ։͋Γ
    ࣮گ΋େ׻ܴͰ͢
    これらは小文字も可

    View Slide

  3. ࿨ా୎ਓ
    UXBEB
    U@XBEB
    UXBEB

    View Slide

  4. Agenda
    ݱঢ়֬ೝ
    ςελϏϦςΟΛ͚͋͜͡Δ
    ϞσϧΛ෼཭͢Δ
    ΞʔΩςΫνϟΛఆΊΔ

    View Slide

  5. γφϦΦ౎ಓ෎ݝΫΠζΛߦ͏
    খ͍͞"MFYB4LJMM։ൃΛҾ͖ܧ͍ͩ

    View Slide

  6. ΞϨΫαɺ౎ಓ෎ݝΫΠζΛ։͍ͯ
    ؆୯ͳΫΠζΛ͠·͠ΐ͏ɻ൪ɻ
    ಢ໦ݝͷݝிॴࡏ஍͸ʁ

    View Slide

  7. Ӊ౎ٶʂ
    ؆୯ͳΫΠζΛ͠·͠ΐ͏ɻ൪ɻ
    ಢ໦ݝͷݝிॴࡏ஍͸ʁ
    ͦ͏Ͱ͢ɻͰ͸൪ɻ
    ԭೄݝͷݝͷՖ͸ʁ

    View Slide

  8. ͕͍ͪ·͢ɻਖ਼ղ͸σΠΰͰ͢ɻ
    Ͱ͸൪ɻἚ৓ݝͷϩʔϚࣈදه͸ʁ
    ͦ͏Ͱ͢ɻͰ͸൪ɻ
    ԭೄݝͷݝͷՖ͸ʁ
    ϋΠϏεΧεʁ

    View Slide

  9. ͦ͏Ͱ͢ɻͰ͸ऴΘΓͰ͢ɻ
    ͋ͳͨ͸఺Ͱͨ͠ɻ
    ਆށʂ
    ɾ
    ɾ
    ɾ
    ͦ͏Ͱ͢ɻͰ͸൪ɻ
    ฌݿݝͷݝிॴࡏ஍͸ʁ

    View Slide

  10. w ߦͷίʔυ
    w ϩδοΫ͸"84-BNCEBͰ࣮૷
    w ݹ͍"MFYB4%, W
    Λ࢖͍ͬͯΔ
    w ࠓޙ͍ͭ͘΋ͷػೳ௥Ճ͕༧ఆ͞Ε͍ͯΔ
    w ओཁͳϩδοΫ͸ॳճىಈॲཧ
    2VJ[*OUFOU
    ͱճ౴डཧ "OTXFS*OUFOU

    w ࣭໰σʔλ͸KTPOͰ؅ཧ͞Ε͍ͯΔ
    'use strict';
    var Alexa = require('alexa-sdk');
    var questions = require('./questions.json');
    var handlers = {
    QuizIntent: function () { // 初期状態
    this.attributes['advance'] = 1; // 進行状況を初期化
    this.attributes['score'] = 0; // 得点を初期化
    var random = Math.floor(Math.random() * questions.length);
    this.attributes['itemIndex'] = random; // 配列番号をitemIndexに保存
    var message = `簡単なクイズをしましょう。1番。 ${questions[random].q}`;
    var reprompt = `1番。 ${questions[random].q}`;
    this.emit(':ask', message, reprompt); // 相手の回答を待つ
    },
    AnswerIntent: function () { // ユーザからの返答により起動される
    // スロットから回答を取得
    var usersAnswer = this.event.request.intent.slots.Answer.value;
    var currentQuestion = questions[this.attributes['itemIndex']];
    var resultMessage;
    if (currentQuestion.a === usersAnswer) { // 正解の場合
    resultMessage = 'そうです。 では';
    this.attributes['score']++;
    } else { // 不正解の場合
    resultMessage = `ちがいます。正解は${currentQuestion.a}です。 では`;
    }
    if (this.attributes['advance'] < 7) { // 続きの問題がある場合
    this.attributes['advance']++;
    var random = Math.floor(Math.random() * questions.length);
    this.attributes['itemIndex'] = random;
    var reprompt = `${this.attributes['advance']}番。 ${questions[random].q}`;
    this.emit(':ask', resultMessage + reprompt, reprompt); // 会話を続ける
    } else { // 全ての問題が終了した場合
    var endMessage = `終わりです。あなたは${this.attributes['score']}点でした。`;
    this.emit(':tell', resultMessage + endMessage); // 会話を終える
    }
    },
    'AMAZON.RepeatIntent': function () {
    var speechOutput = `${this.attributes['advance']}番。 ${questions[this.attributes['itemIndex']].q}`;
    this.emit(':ask', speechOutput, speechOutput);
    },
    'AMAZON.HelpIntent': function () {
    var speechOutput = 'クイズが出題されたらそれに答えてください。それでは始めましょう。「スタート」と言ってください。 ';
    this.emit(':ask', speechOutput, speechOutput);
    },
    'AMAZON.CancelIntent': function () {
    this.response.speak('終わります。');
    this.emit(':responseReady');
    },
    'AMAZON.StopIntent': function () {
    this.response.speak('終わります。');
    this.emit(':responseReady');
    },
    LaunchRequest: function () {
    this.emit('QuizIntent');
    },
    'AMAZON.StartOverIntent': function () {
    this.emit('QuizIntent');
    },
    Unhandled: function () {
    this.emit(':tell', 'すみません、わかりませんでした。終わります。');
    }
    };
    exports.handler = function (event, context, callback) {
    var alexa = Alexa.handler(event, context);
    alexa.registerHandlers(handlers);
    alexa.execute();
    };
    Ҿ͖ܧ͍ͩίʔυ

    View Slide

  11. QuizIntent: function () { // 初期状態
    this.attributes['advance'] = 1; // 進行状況を初期化
    this.attributes['score'] = 0; // 得点を初期化
    var random = Math.floor(Math.random() * questions.length);
    this.attributes['itemIndex'] = random; // 配列番号をitemIndexに保存
    var message = `簡単なクイズをしましょう。1番。 ${questions[random].q}`;
    var reprompt = `1番。 ${questions[random].q}`;
    this.emit(':ask', message, reprompt); // 相手の回答を待つ
    },
    ॳճىಈॲཧ
    this.attributes に入れたものが
    リクエストを跨いで引き継がれる

    View Slide

  12. AnswerIntent: function () { // ユーザからの返答により起動される
    // スロットから回答を取得
    var usersAnswer = this.event.request.intent.slots.Answer.value;
    var currentQuestion = questions[this.attributes['itemIndex']];
    var resultMessage;
    if (currentQuestion.a === usersAnswer) { // 正解の場合
    resultMessage = 'そうです。 では';
    this.attributes['score']++;
    } else { // 不正解の場合
    resultMessage = `ちがいます。正解は${currentQuestion.a}です。 では`;
    }
    if (this.attributes['advance'] < 7) { // 続きの問題がある場合
    this.attributes['advance']++;
    var random = Math.floor(Math.random() * questions.length);
    this.attributes['itemIndex'] = random;
    var reprompt = `${this.attributes['advance']}番。 ${questions[random].q}`;
    this.emit(':ask', resultMessage + reprompt, reprompt); // 会話を続ける
    } else { // 全ての問題が終了した場合
    var endMessage = `終わりです。あなたは${this.attributes['score']}点でした。`;
    this.emit(':tell', resultMessage + endMessage); // 会話を終える
    }
    },
    ճ౴डཧଆ

    View Slide

  13. {
    "q": "茨城県の県庁所在地は?",
    "a": "水戸",
    "g": "PrefecturalOfficeLocation"
    },
    {
    "q": "茨城県の県の花は?",
    "a": "バラ",
    "g": "PrefectureFlower"
    },
    {
    "q": "茨城県のローマ字表記は?",
    "a": "ibaraki",
    "g": "Romanization"
    },
    {
    "q": "茨城県の都道府県コード番号は?",
    "a": "8",
    "g": "PrefectureOrder"
    },
    {
    "q": "栃木県の県庁所在地は?",
    "a": "宇都宮",
    "g": "PrefecturalOfficeLocation"
    },
    ࣭໰σʔλ KTPO

    View Slide

  14. ͳ͓ɺςετίʔυ͸ແ͍

    View Slide

  15. Agenda
    ݱঢ়֬ೝ
    ςελϏϦςΟΛ͚͋͜͡Δ
    ϞσϧΛ෼཭͢Δ
    ΞʔΩςΫνϟΛఆΊΔ

    View Slide

  16. લઢج஍͕ཉ͍͠
    wςετ͕ͳ͍ͱ҆શͳมߋͱࠓޙͷ։ൃܧଓ
    ͕೉͍͠
    w໢ཏੑ͸͍Βͳ͍ɻ೴ఱؾͳਖ਼ৗܥ )BQQZ
    1BUI
    Ͱ͍͍ͷͰɺಈ͘ςετ͕ཉ͍͠
    w·ͣ͸ςετΛಈ͔͢ͱ͜Ζ·Ͱ͍͖࣋ͬͯ
    ͍ͨ

    View Slide

  17. ީิ͸ͭ
    w BMFYBTLJMMUFTUGSBNFXPSL
    w ߴػೳ͔ͭந৅Խ͞Ε͓ͯΓɺ͔Ώ͍ͱ͜Ζʹख͕ಧ͖ʹ͍͘
    w &BTZࢦ޲
    w BXTMBNCEBNPDLDPOUFYU
    w ػೳ͕গͳ͘ɺϨΠϠ͕ബ͍
    w 4JNQMFࢦ޲
    w ͜͜͸ϨΨγʔαόϯφɻ৘ใͷগͳ͍ઓ৔ʹ͸খ͘͞௚ަੑͷߴ͍
    4JNQMFͳಓ۩͕ཉ͍͠

    View Slide

  18. const assert = require('assert').strict;
    const index = require('../index');
    const context = require('aws-lambda-mock-context');
    describe('LaunchRequest を起動して最初の問題を出題', () => {
    let speechResponse;
    before((done) => {
    const ctx = context();
    index.handler(require('./fixtures/launch.json'), ctx);
    ctx.Promise.then(resp => {
    speechResponse = resp;
    done();
    }).catch(err => done(err));
    });
    it('handler の response', () => {
    assert(speechResponse !== undefined);
    });
    });
    BXTMBNCEBNPDLDPOUFYUͰςετΛॻ͍ͯΈΔ

    View Slide

  19. {
    "version": "1.0",
    "session": {
    "new": true,
    "sessionId": "amzn1.echo-api.session.XXXXXXXX",
    "application": {
    "applicationId": "amzn1.ask.skill.XXXXXXXX"
    },
    "attributes": {
    },
    "user": {
    "userId": "amzn1.ask.account.XXXXXXXX"
    }
    },
    "request": {
    "type": "IntentRequest",
    "requestId": "amzn1.echo-api.request.XXXXXXXX",
    "timestamp": "2018-10-26T11:38:40Z",
    "locale": "ja-JP",
    "intent": {
    "name": "LaunchRequest",
    "confirmationStatus": "NONE"
    }
    }
    } ϦΫΤετΠϕϯτͷKTPO͸ݟΑ͏ݟ·ͶͰखͰ࡞Δ

    View Slide

  20. $ npm test
    > mocha --require intelli-espower-loader test
    LaunchRequest を起動して最初の問題を出題
    ✓ handler の response
    1 passing (19ms)

    (
    SFFO
    Α͠Α͠ಈͧ͘

    View Slide

  21. });
    it('handler の response', () => {
    assert.deepEqual(speechResponse, {
    version: '1.0',
    response: {
    shouldEndSession: false,
    outputSpeech: {
    type: 'SSML',
    ssml: ' 簡単なクイズをしましょう。1番。 青森県の県庁所在地は? '
    },
    reprompt: {
    outputSpeech: {
    type: 'SSML',
    ssml: ' 1番。 青森県の県庁所在地は? '
    }
    }
    },
    sessionAttributes: {
    advance: 1,
    score: 0,
    itemIndex: 4
    },
    userAgent: 'ask-nodejs/1.0.25 Node/v8.10.0'
    });
    });
    });
    VOEFpOFEͰ͸βϧͳͷͰ׬શҰகͷςετʹॻ͖׵͑Δ

    View Slide

  22. $ npm test
    > mocha --require intelli-espower-loader test
    LaunchRequest を起動して最初の問題を出題
    1) handler の response
    0 passing (33ms)
    1 failing

    3
    FE

    View Slide

  23. + expected - actual
    {
    "response": {
    "outputSpeech": {
    - "ssml": " 簡単なクイズをしましょう。1番。 福井県の都道府県コード番号は? "
    + "ssml": " 簡単なクイズをしましょう。1番。 青森県の県庁所在地は? "
    "type": "SSML"
    }
    "reprompt": {
    "outputSpeech": {
    - "ssml": " 1番。 福井県の都道府県コード番号は? "
    + "ssml": " 1番。 青森県の県庁所在地は? "
    "type": "SSML"
    }
    }
    "shouldEndSession": false
    }
    "sessionAttributes": {
    "advance": 1
    - "itemIndex": 71
    + "itemIndex": 4
    "score": 0
    }
    "userAgent": "ask-nodejs/1.0.25 Node/v8.10.0"
    "version": "1.0"
    3
    FE
    ग़ྗ͕ظ଴஋ͱ׬શҰக͠ͳ͍

    View Slide

  24. QuizIntent: function () { // 初期状態
    this.attributes['advance'] = 1; // 進行状況を初期化
    this.attributes['score'] = 0; // 得点を初期化
    var random = Math.floor(Math.random() * questions.length);
    this.attributes['itemIndex'] = random; // 配列番号をitemIndexに保存
    var message = `簡単なクイズをしましょう。1番。 ${questions[random].q}`;
    var reprompt = `1番。 ${questions[random].q}`;
    this.emit(':ask', message, reprompt); // 相手の回答を待つ
    },
    ϥϯμϜੑΛ൐͏ϩδοΫ͕ࣦഊͷݩڟͩͬͨ
    3
    FE

    View Slide

  25. ϨΨγʔίʔυͷδϨϯϚ
    lίʔυΛมߋ͢ΔͨΊʹ͸ςετΛ੔උ͢Δ
    ඞཁ͕͋Δɻଟ͘ͷ৔߹ɺςετΛ੔උ͢Δ
    ͨΊʹ͸ɺίʔυΛมߋ͢Δඞཁ͕͋Δz

    View Slide

  26. ઀߹෦ʢ4FBNʣ
    l઀߹෦ʢ4FBNʣͱ͸ɺͦͷ৔ॴΛ௚઀ฤू
    ͠ͳͯ͘΋ɺϓϩάϥϜͷৼΔ෣͍Λม͑Δ
    ͜ͱͷͰ͖Δ৔ॴͰ͋Δz

    View Slide

  27. var createHandler = function (getNextItemIndex) {
    return function (event, context, callback) {
    var alexa = Alexa.handler(event, context);
    alexa.registerHandlers(createHandlers(getNextItemIndex));
    alexa.execute();
    };
    };
    exports.createHandler = createHandler;
    exports.handler = function () {
    var getNextItemIndex = () => Math.floor(Math.random() * questions.length);
    return createHandler(getNextItemIndex);
    }();
    exports.handler = function (event, context, callback) {
    var alexa = Alexa.handler(event, context);
    alexa.registerHandlers(handlers);
    alexa.execute();
    };
    ϥϯμϜੑΛ൐͏ؔ਺Λ֎͔Βࠩ͠ࠐΊΔΑ͏ʹ͢Δ
    変更前
    変更後

    View Slide

  28. -var handlers = {
    +var createHandlers = function (getNextItemIndex) {
    +
    +return {
    QuizIntent: function () { // 初期状態
    this.attributes['advance'] = 1; // 進行状況を初期化
    this.attributes['score'] = 0; // 得点を初期化
    - var random = Math.floor(Math.random() * questions.length);
    + var random = getNextItemIndex();
    this.attributes['itemIndex'] = random; // 配列番号をitemIndexに保存
    var message = `簡単なクイズをしましょう。1番。 ${questions[random].q}`;
    var reprompt = `1番。 ${questions[random].q}`;
    @@ -29,7 +31,7 @@ var handlers = {
    if (this.attributes['advance'] < 7) { // 続きの問題がある場合
    this.attributes['advance']++;
    - var random = Math.floor(Math.random() * questions.length);
    + var random = getNextItemIndex();
    this.attributes['itemIndex'] = random;
    var reprompt = `${this.attributes['advance']}番。 ${questions[random].q}`;
    this.emit(':ask', resultMessage + reprompt, reprompt); // 会話を続ける
    @@ -65,9 +67,18 @@ var handlers = {
    this.emit(':tell', 'すみません、わかりませんでした。終わります。');
    ౉͞Εͨؔ਺Λ࢖͏Α͏ʹ
    ॱ࣍ॻ͖׵͑Δ

    View Slide

  29. before((done) => {
    const ctx = context();
    const getNextItemIndex = () => 4;
    const handler = index.createHandler(getNextItemIndex);
    handler(require('./fixtures/launch.json'), ctx);
    ctx.Promise.then(resp => {
    speechResponse = resp;
    done();
    }).catch(err => done(err));
    });
    ςετ͔Β͸ϥϯμϜͰ͸ͳ͘ݻఆ஋Λฦؔ͢਺Λࠩ͠ࠐΉ

    View Slide

  30. $ npm test
    > mocha --require intelli-espower-loader test
    LaunchRequest を起動して最初の問題を出題
    ✓ handler の response
    1 passing (19ms)

    ग़ྗ͕ظ଴஋ͱ׬શҰக͢ΔΑ͏ʹͳͬͨ

    View Slide

  31. $ npm test
    > mocha --require intelli-espower-loader test
    LaunchRequest を起動して最初の問題を出題
    ✓ handler の response
    問題に正解した場合
    ✓ handler の response
    問題に不正解の場合
    ✓ handler の response
    3 passing (23ms)

    (
    SFFO
    ·ͣ͸ॳճىಈ࣌ɺਖ਼ղɺෆਖ਼ղͷέʔεΛ੔උ

    View Slide

  32. ͔Ζ͏ͯ͡ಆ͏४උ͕੔ͬͨ

    View Slide

  33. QuizIntent: function () { // 初期状態
    this.attributes['advance'] = 1; // 進行状況を初期化
    this.attributes['score'] = 0; // 得点を初期化
    var random = getNextItemIndex();
    this.attributes['itemIndex'] = random; // 配列番号をitemIndexに保存
    var message = `簡単なクイズをしましょう。1番。 ${questions[random].q}`;
    var reprompt = `1番。 ${questions[random].q}`;
    this.emit(':ask', message, reprompt); // 相手の回答を待つ
    },
    AnswerIntent: function () { // ユーザからの返答により起動される
    // スロットから回答を取得
    var usersAnswer = this.event.request.intent.slots.Answer.value;
    var currentQuestion = questions[this.attributes['itemIndex']];
    var resultMessage;
    if (currentQuestion.a === usersAnswer) { // 正解の場合
    resultMessage = 'そうです。 では';
    this.attributes['score']++;
    } else { // 不正解の場合
    resultMessage = `ちがいます。正解は${currentQuestion.a}です。 では`;
    }
    if (this.attributes['advance'] < 7) { // 続きの問題がある場合
    this.attributes['advance']++;
    var random = getNextItemIndex();
    this.attributes['itemIndex'] = random;
    var reprompt = `${this.attributes['advance']}番。 ${questions[random].q}`;
    this.emit(':ask', resultMessage + reprompt, reprompt); // 会話を続ける
    } else { // 全ての問題が終了した場合
    var endMessage = `終わりです。あなたは${this.attributes['score']}点でした。`;
    this.emit(':tell', resultMessage + endMessage); // 会話を終える
    }
    },
    ݱ࣌఺ͷίʔυओཁ෦

    View Slide

  34. ͜͜Ͱ࢓༷มߋͰ͢
    wؒҧ͑ͯ΋ɺಉ͡໰୊Λճ·Ͱ܁Γฦ͍ͨ͠
    wؒҧ͑ͨճ਺ʹԠͯ͡ϝοηʔδΛม͍͑ͨ
    wਖ਼ղͨ͠Β࣍ͷ໰୊ʹਐΉ
    wճؒҧ͑ͨΒෆਖ਼ղͱͯ࣍͠ͷ໰୊ʹਐΉ
    ͕ͩɺ͍·΍ࢲͨͪʹ͸ςετ͕͋Δʂ
    ΍͍ͬͯͧ͘ʂʂ

    View Slide

  35. ࣍ͷ໨ඪΛߟ͑Δ
    ͦͷ໨ඪΛࣔ͢ςετΛॻ͘
    ͦͷςετΛ࣮ߦࣦͯ͠ഊͤ͞Δ 3FE

    ໨తͷίʔυΛॻ͘
    Ͱॻ͍ͨςετΛ੒ޭͤ͞Δ (SFFO

    ςετ͕௨Δ··ͰϦϑΝΫλϦϯάΛߦ͏
    3FGBDUPS

    ̍ʙΛ܁Γฦ͢
    5%%ͷαΠΫϧ

    View Slide

  36. -describe('問題に不正解の場合', () => {
    +describe('1回目の不正解の場合', () => {
    let speechResponse;
    before((done) => {
    const ctx = context();
    const getNextItemIndex = () => 4;
    const handler = index.createHandler(getNextItemIndex);
    - handler(require('./fixtures/req_incorrect.json'), ctx);
    + const req = require('./fixtures/req_incorrect.json');
    + Object.assign(req.session.attributes, { // 事前状態
    + advance: 2,
    + score: 1,
    + accumIncorrects: 0,
    + itemIndex: 8
    + });
    + handler(req, ctx);
    ctx.Promise.then(resp => {
    speechResponse = resp;
    done();
    }).catch(err => done(err));
    });
    + it('連続不正解数が増えていること', () => {
    + assert(speechResponse.sessionAttributes.accumIncorrects === 1);
    + });
    - it('handler の response', () => {
    + it.skip('handler の response', () => {
    const expected = {
    3
    FE
    ·ͣ͸ࣦഊ͢ΔςετΛॻ͘
    テストコード内で事前状態を作る

    View Slide

  37. LaunchRequest を起動して最初の問題を出題
    ✓ handler の response
    問題に正解した場合
    ✓ handler の response
    1回目の不正解の場合
    - handler の response
    1) 連続不正解数が増えていること
    2 passing (33ms)
    1 pending
    1 failing
    1) 1回目の不正解の場合
    連続不正解数が増えていること:
    AssertionError [ERR_ASSERTION]: # test/test.js:128
    assert(speechResponse.sessionAttributes.accumIncorrects === 1)
    | | | |
    | | 0 false
    | Object{advance:3,score:1,accumIncorrects:0,itemIndex:4}
    Object{version:"1.0",response:#Object#,sessionAttributes:#Object#,userAgent:"ask-nodejs/1.0.25 Node/
    v8.10.0"}
    [number] 1
    => 1
    [number] speechResponse.sessionAttributes.accumIncorrects
    => 0
    3
    FE
    ςετͷࣦഊΛ֬ೝ͢Δ
    完全一致のテストはしばらくスキップ

    View Slide

  38. + var shouldRepeatSameQuestion = false;
    var resultMessage;
    if (currentQuestion.a === usersAnswer) { // 正解の場合
    resultMessage = 'そうです。 では';
    this.attributes['score']++;
    } else { // 不正解の場合
    + this.attributes['accumIncorrects']++;
    + shouldRepeatSameQuestion = true;
    resultMessage = `ちがいます。正解は${currentQuestion.a}です。 では`;
    }
    - if (this.attributes['advance'] < 7) { // 続きの問題がある場合
    + if (shouldRepeatSameQuestion) {
    + var reprompt = `${this.attributes['advance']}番。 ${currentQuestion.q}`;
    + this.emit(':ask', resultMessage, reprompt);
    + } else if (this.attributes['advance'] < 7) { // 続きの問題がある場合
    this.attributes['advance']++;
    ࣦഊ͍ͯ͠ΔςετΛ੒ޭͤ͞ΔͨΊͷ࠷খݶͷίʔυΛॻ͘

    View Slide

  39. $ npm test
    > mocha --require intelli-espower-loader test
    LaunchRequest を起動して最初の問題を出題
    ✓ handler の response
    問題に正解した場合
    ✓ handler の response
    1回目の不正解の場合
    ✓ 連続不正解数が増えていること
    - handler の response
    3 passing (21ms)
    1 pending
    (
    SFFO
    ςετͷ੒ޭΛ֬ೝ͢Δ

    View Slide

  40. assert(speechResponse.sessionAttributes.accumIncorrects === 1);
    });
    + it('返答の音声内容が1回目の不正解に伴う内容であること', () => {
    + assert(speechResponse.response.outputSpeech.ssml === ' 久慈?
    もう一度言ってください。岩手県の県庁所在地は? ');
    + });
    it.skip('handler の response', () => {
    const expected = {
    version: '1.0',
    $ npm test
    > mocha --require intelli-espower-loader test
    LaunchRequest を起動して最初の問題を出題
    ✓ handler の response
    問題に正解した場合
    ✓ handler の response
    1回目の不正解の場合
    ✓ 連続不正解数が増えていること
    1) 返答の音声内容が1回目の不正解に伴う内容であること
    - handler の response
    3 passing (37ms)
    1 pending
    1 failing
    3
    FE
    ·ࣦͣഊ͢ΔςετΛॻ͘

    View Slide

  41. $ npm test
    > mocha --require intelli-espower-loader test
    LaunchRequest を起動して最初の問題を出題
    ✓ handler の response
    問題に正解した場合
    ✓ handler の response
    1回目の不正解の場合
    ✓ 連続不正解数が増えていること
    ✓ 返答の音声内容が1回目の不正解に伴う内容であること
    - handler の response
    4 passing (21ms)
    1 pending
    @@ -29,7 +29,7 @@ return {
    } else { // 不正解の場合
    this.attributes['accumIncorrects']++;
    shouldRepeatSameQuestion = true;
    - resultMessage = `ちがいます。正解は${currentQuestion.a}です。 では`;
    + resultMessage = `${usersAnswer}? もう一度言ってください。${currentQuestion.q}`;
    }
    if (shouldRepeatSameQuestion) {
    (
    SFFO
    ੒ޭͤ͞ΔͨΊͷ࠷খݶͷίʔυΛॻ͘

    View Slide

  42. い。岩手県の県庁所在地は? ');
    });
    + it('進行状況が進んでいないこと', () => {
    + assert(speechResponse.sessionAttributes.advance === 2);
    + });
    + it('得点が変わらないこと', () => {
    + assert(speechResponse.sessionAttributes.score === 1);
    + });
    + it('問題番号が変わらないこと', () => {
    + assert(speechResponse.sessionAttributes.itemIndex === 8);
    + });
    it.skip('handler の response', () => {
    const expected = {
    LaunchRequest を起動して最初の問題を出題
    ✓ handler の response
    問題に正解した場合
    ✓ handler の response
    1回目の不正解の場合
    ✓ 進行状況が進んでいないこと
    ✓ 得点が変わらないこと
    ✓ 連続不正解数が増えていること
    ✓ 問題番号が変わらないこと
    ✓ 返答の音声内容が1回目の不正解に伴う内容であること
    - handler の response
    7 passing (22ms)
    1 pending
    (
    SFFO
    3FE(SFFO3FGBDUPS
    ͷճసΛ܁Γฦ͢

    View Slide

  43. (
    SFFO
    });
    - it.skip('handler の response', () => {
    + it('handler の response', () => {
    const expected = {
    version: '1.0',
    response: {
    shouldEndSession: false,
    outputSpeech: {
    type: 'SSML',
    - ssml: ' ちがいます。正解は盛岡です。 では3番。 青森県の県庁所在地は? '
    + ssml: ' 久慈? もう一度言ってください。岩手県の県庁所在地は? '
    },
    reprompt: {
    outputSpeech: {
    type: 'SSML',
    - ssml: ' 3番。 青森県の県庁所在地は? '
    + ssml: ' 2番。 岩手県の県庁所在地は? '
    }
    }
    },
    sessionAttributes: {
    - advance: 3,
    + advance: 2,
    score: 1,
    - itemIndex: 4
    + accumIncorrects: 1,
    + itemIndex: 8
    },
    1回目の不正解の場合
    ✓ 連続不正解数が増えていること
    ✓ 返答の音声内容が1回目の不正解に伴う内容であること
    ✓ 進行状況が進んでいないこと
    ✓ 得点が変わらないこと
    ✓ 問題番号が変わらないこと
    ✓ handler の response
    8 passing (20ms)
    ׬શҰகͷςετ΋
    άϦʔϯ෮ؼ

    View Slide

  44. describe('2回目の不正解の場合', () => {
    let speechResponse;
    before((done) => {
    const ctx = context();
    const getNextItemIndex = () => 4;
    const handler = index.createHandler(getNextItemIndex);
    const req = require('./fixtures/req_incorrect.json');
    Object.assign(req.session.attributes, {
    advance: 2,
    score: 1,
    accumIncorrects: 1,
    itemIndex: 8
    });
    handler(req, ctx);
    ctx.Promise.then(resp => {
    speechResponse = resp;
    done();
    }).catch(err => done(err));
    });
    it('連続不正解数が増えていること', () => {
    assert(speechResponse.sessionAttributes.accumIncorrects === 2);
    });
    it('返答の音声内容が2回目の不正解に伴う内容であること', () => {
    assert(speechResponse.response.outputSpeech.ssml === ' 私には「久慈」と聞こえましたが、それは正しくありません。
    もう一度言ってください。岩手県の県庁所在地は? ');
    });
    it('進行状況が進んでいないこと', () => {
    assert(speechResponse.sessionAttributes.advance === 2);
    });
    it('得点が変わらないこと', () => {
    assert(speechResponse.sessionAttributes.score === 1);
    });
    it('問題番号が変わらないこと', () => {
    assert(speechResponse.sessionAttributes.itemIndex === 8);
    });
    });
    3
    FE
    1回目の不正解の場合
    ✓ 連続不正解数が増えていること
    ✓ 返答の音声内容が1回目の不正解に伴う内容であること
    ✓ 進行状況が進んでいないこと
    ✓ 得点が変わらないこと
    ✓ 問題番号が変わらないこと
    ✓ handler の response
    2回目の不正解の場合
    ✓ 連続不正解数が増えていること
    1) 返答の音声内容が2回目の不正解に伴う内容であること
    ✓ 進行状況が進んでいないこと
    ✓ 得点が変わらないこと
    ✓ 問題番号が変わらないこと
    12 passing (47ms)
    1 failing
    3FE(SFFO3FGBDUPS
    ͷճసΛ܁Γฦ͢

    View Slide

  45. --- a/index.js
    +++ b/index.js
    @@ -29,7 +29,11 @@ return {
    } else { // 不正解の場合
    this.attributes['accumIncorrects']++;
    shouldRepeatSameQuestion = true;
    - resultMessage = `${usersAnswer}? もう一度言ってください。${currentQuestion.q}`;
    + if (this.attributes['accumIncorrects'] == 1) {
    + resultMessage = `${usersAnswer}? もう一度言ってください。${currentQuestion.q}`;
    + } else if (this.attributes['accumIncorrects'] == 2) {
    + resultMessage = `私には「${usersAnswer}」と聞こえましたが、それは正しくありませ
    ん。もう一度言ってください。${currentQuestion.q}`;
    + }
    }
    if (shouldRepeatSameQuestion) {
    1回目の不正解の場合
    ✓ 連続不正解数が増えていること
    ✓ 返答の音声内容が1回目の不正解に伴う内容であること
    ✓ 進行状況が進んでいないこと
    ✓ 得点が変わらないこと
    ✓ 問題番号が変わらないこと
    ✓ handler の response
    2回目の不正解の場合
    ✓ 連続不正解数が増えていること
    ✓ 返答の音声内容が2回目の不正解に伴う内容であること
    ✓ 進行状況が進んでいないこと
    ✓ 得点が変わらないこと
    ✓ 問題番号が変わらないこと
    13 passing (22ms)
    (
    SFFO
    3FE(SFFO3FGBDUPS
    ͷճసΛ܁Γฦ͢

    View Slide

  46. --- a/index.js
    +++ b/index.js
    @@ -29,11 +29,14 @@ return {
    } else { // 不正解の場合
    this.attributes['accumIncorrects']++;
    shouldRepeatSameQuestion = true;
    - if (this.attributes['accumIncorrects'] == 1) {
    - resultMessage = `${usersAnswer}? もう一度言ってください。${currentQuestion.q}`;
    - } else if (this.attributes['accumIncorrects'] == 2) {
    - resultMessage = `私には「${usersAnswer}」と聞こえましたが、それは正しくありません。もう一度言っ
    てください。${currentQuestion.q}`;
    - }
    + switch (this.attributes['accumIncorrects']) {
    + case 1:
    + resultMessage = `${usersAnswer}? もう一度言ってください。${currentQuestion.q}`;
    + break;
    + case 2:
    + resultMessage = `私には「${usersAnswer}」と聞こえましたが、それは正しくありません。もう一度言っ
    てください。${currentQuestion.q}`;
    + break;
    + }
    }
    if (shouldRepeatSameQuestion) {
    3
    FGBDUPS
    3FE(SFFO3FGBDUPS
    ͷճసΛ܁Γฦ͢

    View Slide

  47. 3
    FE
    describe('3回目の不正解の場合', () => {
    let speechResponse;
    before((done) => {
    const ctx = context();
    const getNextItemIndex = () => 4;
    const handler = index.createHandler(getNextItemIndex);
    const req = require('./fixtures/req_incorrect.json');
    Object.assign(req.session.attributes, {
    advance: 2,
    score: 1,
    accumIncorrects: 2,
    itemIndex: 8
    });
    handler(req, ctx);
    ctx.Promise.then(resp => {
    speechResponse = resp;
    done();
    }).catch(err => done(err));
    });
    it('連続不正解数が0に戻っていること', () => {
    assert(speechResponse.sessionAttributes.accumIncorrects === 0);
    });
    it('返答の音声内容が3回目の不正解に伴う内容であること', () => {
    assert(speechResponse.response.outputSpeech.ssml === ' ちが
    います。正解は盛岡です。 では3番。 青森県の県庁所在地は? ');
    });
    it('次の問題に進んでいるので進行状況が進んでいること', () => {
    assert(speechResponse.sessionAttributes.advance === 3);
    });
    it('得点が変わらないこと', () => {
    assert(speechResponse.sessionAttributes.score === 1);
    });
    it('次の問題に進んでいるので問題番号が変わっていること', () => {
    assert(speechResponse.sessionAttributes.itemIndex === 4);
    });
    });
    1回目の不正解の場合
    ✓ 連続不正解数が増えていること
    ✓ 返答の音声内容が1回目の不正解に伴う内容であること
    ✓ 進行状況が進んでいないこと
    ✓ 得点が変わらないこと
    ✓ 問題番号が変わらないこと
    ✓ handler の response
    2回目の不正解の場合
    ✓ 連続不正解数が増えていること
    ✓ 返答の音声内容が2回目の不正解に伴う内容であること
    ✓ 進行状況が進んでいないこと
    ✓ 得点が変わらないこと
    ✓ 問題番号が変わらないこと
    3回目の不正解の場合
    1) 連続不正解数が0に戻っていること
    2) 返答の音声内容が3回目の不正解に伴う内容であること
    3) 次の問題に進んでいるので進行状況が進んでいること
    ✓ 得点が変わらないこと
    4) 次の問題に進んでいるので問題番号が変わっていること
    14 passing (52ms)
    4 failing

    View Slide

  48. diff --git a/index.js b/index.js
    index 65f66d8..4498974 100644
    --- a/index.js
    +++ b/index.js
    @@ -28,13 +28,18 @@ return {
    this.attributes['score']++;
    } else { // 不正解の場合
    this.attributes['accumIncorrects']++;
    - shouldRepeatSameQuestion = true;
    switch (this.attributes['accumIncorrects']) {
    case 1:
    resultMessage = `${usersAnswer}?
    + shouldRepeatSameQuestion = true;
    break;
    case 2:
    resultMessage = `私には「${usersAnswer}」と聞こえましたが、それは正しくあり
    ません。もう一度言ってください。${currentQuestion.q}`;
    + shouldRepeatSameQuestion = true;
    + break;
    + default:
    + resultMessage = `ちがいます。正解は${currentQuestion.a}です。 では`;
    + this.attributes['accumIncorrects'] = 0;
    break;
    1回目の不正解の場合
    ✓ 連続不正解数が増えていること
    ✓ 返答の音声内容が1回目の不正解に伴う内容であること
    ✓ 進行状況が進んでいないこと
    ✓ 得点が変わらないこと
    ✓ 問題番号が変わらないこと
    ✓ handler の response
    2回目の不正解の場合
    ✓ 連続不正解数が増えていること
    ✓ 返答の音声内容が2回目の不正解に伴う内容であること
    ✓ 進行状況が進んでいないこと
    ✓ 得点が変わらないこと
    ✓ 問題番号が変わらないこと
    3回目の不正解の場合
    ✓ 連続不正解数が0に戻っていること
    ✓ 返答の音声内容が3回目の不正解に伴う内容であること
    ✓ 次の問題に進んでいるので進行状況が進んでいること
    ✓ 得点が変わらないこと
    ✓ 次の問題に進んでいるので問題番号が変わっていること
    18 passing (19ms)
    (
    SFFO

    View Slide

  49. $ npm test
    LaunchRequest を起動して最初の問題を出題
    ✓ 連続不正解数が0であること
    ✓ 返答の音声内容が初回の出題に伴う内容であること
    ✓ 進行状況は1であること
    ✓ 得点は0であること
    ✓ 出題された問題番号は 4
    ✓ handler の response
    問題に正解した場合
    ✓ 連続不正解数が0に戻っていること
    ✓ 返答の音声内容が問題の正解に伴う内容であること
    ✓ 次の問題に進んでいるので進行状況が進んでいること
    ✓ 得点が1増えていること
    ✓ 次の問題に進んでいるので問題番号が変わっていること
    ✓ handler の response
    1回目の不正解の場合
    ✓ 連続不正解数が増えていること
    ✓ 返答の音声内容が1回目の不正解に伴う内容であること
    ✓ 進行状況が進んでいないこと
    ✓ 得点が変わらないこと
    ✓ 問題番号が変わらないこと
    ✓ handler の response
    2回目の不正解の場合
    ✓ 連続不正解数が増えていること
    ✓ 返答の音声内容が2回目の不正解に伴う内容であること
    ✓ 進行状況が進んでいないこと
    ✓ 得点が変わらないこと
    ✓ 問題番号が変わらないこと
    3回目の不正解の場合
    ✓ 連続不正解数が0に戻っていること
    ✓ 返答の音声内容が3回目の不正解に伴う内容であること
    ✓ 次の問題に進んでいるので進行状況が進んでいること
    ✓ 得点が変わらないこと
    ✓ 次の問題に進んでいるので問題番号が変わっていること
    最終問題に正解した場合
    ✓ shouldEndSession が true になっていること
    ✓ 連続不正解数が0に戻っていること
    ✓ 返答の音声内容がクイズ終了を知らせる内容であること
    ✓ 進行状況が変わらないこと
    ✓ 得点が1増えていること
    ✓ 問題番号が変わらないこと
    ✓ handler の response
    35 passing (32ms)
    (
    SFFO
    ΍͔ͬͨʂʁ
    ˞ϑϥάɹɹɹɹ
    5%%ͷճసΛճ͠ଓ͚ɺ͞
    ͖΄Ͳͷ࢓༷มߋΛຬͨ͢
    ςετͱίʔυ͕Φʔϧά
    Ϧʔϯ·Ͱ΍͖ͬͯͨ
    35 passing (32ms)

    View Slide

  50. ͕ͩɺͪΐͬͱ଴ͬͯ΄͍͠

    View Slide

  51. ઃܭ͸ྑ͘ͳ͍ͬͯΔͩΖ͏͔ʁ
    IUUQTUXPQBHJMFFTNDPKQXIBUEPXFOFFEGPSHSPXUIPGGVUVSFDCBGF
    رബԽͨ͠5%%ɺϓϩμΫτͷ੒௕ͷͨΊʹඞཁͳ΋ͷ͸ʁʙʰ݈શͳϏδωεͷܧଓత੒௕ͷͨΊʹ͸݈શͳίʔυ͕ඞཁͩʱରஊʢ̒ʣΑΓ

    View Slide

  52. ઃܭ͸ྑ͘ͳ͍ͬͯΔͩΖ͏͔ʁ
    IUUQTUXPQBHJMFFTNDPKQXIBUEPXFOFFEGPSHSPXUIPGGVUVSFDCBGF
    رബԽͨ͠5%%ɺϓϩμΫτͷ੒௕ͷͨΊʹඞཁͳ΋ͷ͸ʁʙʰ݈શͳϏδωεͷܧଓత੒௕ͷͨΊʹ͸݈શͳίʔυ͕ඞཁͩʱରஊʢ̒ʣΑΓ

    View Slide

  53. ݱ࣌఺ͷίʔυΛݟͯΈΔ
    QuizIntent: function () { // 初期状態
    this.attributes['advance'] = 1; // 進行状況を初期化
    this.attributes['score'] = 0; // 得点を初期化
    this.attributes['accumIncorrects'] = 0; // 連続不正解数を初期化
    var random = getNextItemIndex();
    this.attributes['itemIndex'] = random; // 配列番号をitemIndexに保存
    var message = `簡単なクイズをしましょう。1番。 ${questions[random].q}`;
    var reprompt = `1番。 ${questions[random].q}`;
    this.emit(':ask', message, reprompt); // 相手の回答を待つ
    },
    AnswerIntent: function () { // ユーザからの返答により起動される
    // スロットから回答を取得
    var usersAnswer = this.event.request.intent.slots.Answer.value;
    var currentQuestion = questions[this.attributes['itemIndex']];
    var shouldRepeatSameQuestion = false;
    var resultMessage;
    if (currentQuestion.a === usersAnswer) { // 正解の場合
    resultMessage = 'そうです。 では';
    this.attributes['score']++;
    this.attributes['accumIncorrects'] = 0;
    } else { // 不正解の場合
    this.attributes['accumIncorrects']++;
    switch (this.attributes['accumIncorrects']) {
    case 1:
    resultMessage = `${usersAnswer}? もう一度言ってください。${currentQuestion.q}`;
    shouldRepeatSameQuestion = true;
    break;
    case 2:
    resultMessage = `私には「${usersAnswer}」と聞こえましたが、それは正しくありません。もう一度言ってください。${currentQuestion.q}`;
    shouldRepeatSameQuestion = true;
    break;
    default:
    resultMessage = `ちがいます。正解は${currentQuestion.a}です。 では`;
    this.attributes['accumIncorrects'] = 0;
    break;
    }
    }
    if (shouldRepeatSameQuestion) {
    var reprompt = `${this.attributes['advance']}番。 ${currentQuestion.q}`;
    this.emit(':ask', resultMessage, reprompt);
    } else if (this.attributes['advance'] < 7) { // 続きの問題がある場合
    this.attributes['advance']++;
    var random = getNextItemIndex();
    this.attributes['itemIndex'] = random;
    var reprompt = `${this.attributes['advance']}番。 ${questions[random].q}`;
    this.emit(':ask', resultMessage + reprompt, reprompt); // 会話を続ける
    } else { // 全ての問題が終了した場合
    var endMessage = `終わりです。あなたは${this.attributes['score']}点でした。`;
    this.emit(':tell', resultMessage + endMessage); // 会話を終える
    }
    },
    ͜ɺ͜Ε͸ʜʜ

    View Slide

  54. ϓϩμΫτίʔυͷ࣭͸
    Ή͠Ζ௿Լ͍ͯ͠ΔͷͰ͸ʜʜʁ

    View Slide

  55. lςετͰ͸඼࣭͸্͕Βͳ͍Ͱ͢Αɻ
    ςετ͸͋͘·Ͱ΋඼࣭Λ͋͛Δ͖͔ͬ
    ͚ɻ඼࣭Λ͋͛Δͷ͸ϓϩάϥϛϯάͰ
    ͢ɻ͜Ε͸େੲ͔Βͦ͏ɻz

    View Slide

  56. ͜͜Ͱ௥͍ଧͪΛ͔͚ΔΑ͏ʹ࢓༷มߋ
    w ໰ΛϒϩοΫʢ ʣʹ۠੾ΓɺલͷϒϩοΫͰ
    ؒҧ͑ͨδϟϯϧ ݝிॴࡏ஍౳
    Λ༏ઌͯ͠ग़୊͍ͨ͠
    w ϒϩοΫ಺Ͱಉ͡δϟϯϧ͸ग़͞ͳ͍
    w ࠷ॳͷϒϩοΫͰ͸δϟϯϧ͕ॏͳΒͳ͍Α͏ʹϥϯμ
    Ϝʹ໰ग़͍ͨ͠
    ͑ͬɺ͍·ʜʜʁ

    View Slide

  57. ͞Βʹ௥͍ଧͪΛ͔͚ΔΑ͏ʹαϙʔτऴྃ
    npm WARN deprecated [email protected]: This
    version of the Alexa Skills Kit SDK is no
    longer supported.
    Please use the v2 release found here:
    https://github.com/alexa/alexa-skills-kit-
    sdk-for-nodejs
    ͑ͬɺ͍·ʜʜʁ

    View Slide

  58. ͜Μͳ͸ͣͰ͸ͳ͔ͬͨ

    View Slide

  59. ݱঢ়ͷͭΒΈ
    wͭͷมߋཁҼʢج൫ͷࣄ৘ɺ࢓༷ͷมߋʣ͕୯Ұͷ
    ϓϩμΫτίʔυʹ͢΂ͯ߱Γ͔͔ͬͯ͘Δ
    wج൫ͷΞοϓσʔτͱ࢓༷มߋɺ΋ͪΖΜͲͪ
    Β΋΍Βͳ͚Ε͹ͳΒͳ͍
    wݱঢ়͸ΫϦοΫϋϯυϥʹϏδωεϩδοΫ͕ॻ͔Ε
    ͍ͯΔঢ়ଶʹ౳͍͠ɻ͜ͷ··Ͱ͸ઌ͕ͳ͍
    wϏδωεϩδοΫΛ"MFYB -BNCEB͔ΒҾ͖͸͕ͦ͏

    View Slide

  60. Agenda
    ݱঢ়֬ೝ
    ςελϏϦςΟΛ͚͋͜͡Δ
    ϞσϧΛ෼཭͢Δ
    ΞʔΩςΫνϟΛఆΊΔ

    View Slide

  61. ϞσϧΫϥεΛ෼཭͢Δઓུ
    w͜͜·Ͱॻ͍͖ͯͨ-BNCEBϨϕϧͷςετΛά
    Ϧʔϯʹอͪͳ͕ΒɺϩδοΫΛϞσϧΫϥεʹҾ
    ͖͸͕͍ͯ͘͠
    wϞσϧΫϥε͸ςετۦಈ։ൃͰ৽ن։ൃ͢Δ

    View Slide

  62. const assert = require('assert').strict;
    const {Session} = require('../models');
    describe('Session#start(item)', () => {
    let session;
    beforeEach(() => {
    session = new Session();
    session.start({
    q: '広島県の県の花は?',
    a: 'モミジ',
    g: 'PrefectureFlower'
    });
    });
    it('#message', () => {
    assert(session.message() === '簡単なクイズをしましょう。1番。 広島県の県の花は?');
    });
    it('#reprompt', () => {
    assert(session.reprompt() === '1番。 広島県の県の花は?');
    });
    });
    3
    FE
    "MFYB΍-BNCEBʹґଘ͠ͳ͍Ϣχοτςετ͕ॻ͚Δ

    View Slide

  63. class Session {
    start (item) {
    this.advance = 1;
    this.score = 0;
    this.accumIncorrects = 0;
    this.item = item;
    }
    message () {
    return `簡単なクイズをしましょう。${this.advance}番。 ${this.item.q}`;
    }
    reprompt () {
    return `${this.advance}番。 ${this.item.q}`;
    }
    }
    module.exports = {
    Session
    };
    (
    SFFO
    -BNCEB͔ΒϩδοΫΛίϐʔͯ͠ςετΛ௨͢

    View Slide

  64. var questions = require('./questions.json');
    +const {Session} = require('./models');
    var createHandlers = function (getNextItemIndex) {
    return {
    QuizIntent: function () { // 初期状態
    - this.attributes['advance'] = 1; // 進行状況を初期化
    - this.attributes['score'] = 0; // 得点を初期化
    - this.attributes['accumIncorrects'] = 0; // 連続不正解数を初期化
    var random = getNextItemIndex();
    this.attributes['itemIndex'] = random; // 配列番号をitemIndexに保存
    - var message = `簡単なクイズをしましょう。1番。 ${questions[random].q}`;
    - var reprompt = `1番。 ${questions[random].q}`;
    - this.emit(':ask', message, reprompt); // 相手の回答を待つ
    + const session = new Session();
    + session.start(questions[random]);
    + this.attributes['advance'] = session.advance;
    + this.attributes['score'] = session.score;
    + this.attributes['accumIncorrects'] = session.accumIncorrects;
    + this.emit(':ask', session.message(), session.reprompt()); // 相手の回答を待つ
    },
    (
    SFFO
    -BNCEBͷํ͔ΒϞσϧͷϩδοΫΛ࢖͏Α͏ʹมߋ

    View Slide

  65. describe('Session#receive(answer)', () => {
    let session;
    beforeEach(() => {
    session = new Session();
    session.start({
    q: '広島県の県の花は?',
    a: 'モミジ',
    g: 'PrefectureFlower'
    });
    });
    describe('正解した場合', () => {
    beforeEach(() => {
    session.receive('モミジ');
    });
    it('score が加点されていること', () => {
    assert(session.score === 1);
    });
    });
    });
    3
    FE
    ࣍͸ճ౴डཧΛςετϑΝʔετͰ

    View Slide

  66. class Session {
    start (item) {
    this.advance = 1;
    this.score = 0;
    this.accumIncorrects = 0;
    this.item = item;
    }
    message () {
    return `簡単なクイズをしましょう。${this.advance}番。 ${this.item.q}`;
    }
    reprompt () {
    return `${this.advance}番。 ${this.item.q}`;
    }
    receive (answer) {
    if (this.item.a === answer) {
    this.score += 1;
    }
    }
    }
    module.exports = {
    Session
    };
    (
    SFFO
    γϯϓϧʹάϦʔϯʹ͢Δ

    View Slide

  67. Ҏ͙߱Δ͙Δ
    3FE(SFFO3FGBDUPSͷ
    ճసΛଓ͚·͢
    $ npm run test:models
    初回の出題時
    ✓ 問題番号は 1
    ✓ 得点は 0
    ✓ 連続不正解数は 0
    ✓ 次の問題に進むこと
    ✓ メッセージ
    ✓ reprompt
    3問目に初挑戦する状態
    正解した場合
    ✓ 問題番号がひとつ進むこと
    ✓ 得点が加点されていること
    ✓ 連続不正解数がリセットされていること
    ✓ 次の問題に進むこと
    ✓ 次の問題を設定した後のメッセージ
    不正解の場合
    ✓ 問題番号が変わらないこと
    ✓ 得点が変わらないこと
    ✓ 連続不正解数が増えていること
    ✓ 同じ問題を繰り返すこと
    ✓ メッセージ
    3問目に既に1回不正解の状態
    不正解の場合
    ✓ 問題番号が変わらないこと
    ✓ 得点が変わらないこと
    ✓ 連続不正解数が増えていること
    ✓ 同じ問題を繰り返すこと
    ✓ メッセージ
    3問目に既に2回不正解の状態
    不正解の場合
    ✓ 問題番号がひとつ進むこと
    ✓ 得点が変わらないこと
    ✓ 連続不正解数がリセットされていること
    ✓ 次の問題に進むこと
    ✓ 次の問題を設定した後のメッセージ
    最終問題に初挑戦する状態
    正解した場合
    ✓ isFinished は true
    ✓ 問題番号が変わらないこと
    ✓ 得点が加点されていること
    ✓ 連続不正解数がリセットされていること
    ✓ 次の問題に進まないこと
    ✓ メッセージ
    不正解の場合
    ✓ isFinished は false
    ✓ 問題番号が変わらないこと
    ✓ 得点が変わらないこと
    ✓ 連続不正解数が増えていること
    ✓ 同じ問題を繰り返すこと
    ✓ メッセージ
    最終問題に2回不正解している状態
    正解した場合
    ✓ isFinished は true
    ✓ 問題番号が変わらないこと
    ✓ 得点が加点されていること
    ✓ 連続不正解数がリセットされていること
    ✓ 次の問題に進まないこと
    ✓ メッセージ
    不正解の場合
    ✓ isFinished は true
    ✓ 問題番号が変わらないこと
    ✓ 得点が変わらないこと
    ✓ 連続不正解数がリセットされていること
    ✓ 次の問題に進まないこと
    ✓ メッセージ
    50 passing (59ms)

    View Slide

  68. $ npm run test:models
    > mocha --require intelli-espower-loader test/session_test.js
    初回の出題時
    ✓ 問題番号は 1
    ✓ 得点は 0
    ✓ 連続不正解数は 0
    ✓ 次の問題に進むこと
    ✓ メッセージ
    ✓ reprompt
    3問目に初挑戦する状態
    正解した場合
    ✓ 問題番号がひとつ進むこと
    ✓ 得点が加点されていること
    ✓ 連続不正解数がリセットされていること
    ✓ 次の問題に進むこと
    ✓ 次の問題を設定した後のメッセージ
    不正解の場合
    ✓ 問題番号が変わらないこと
    ✓ 得点が変わらないこと
    ✓ 連続不正解数が増えていること
    ✓ 同じ問題を繰り返すこと
    ✓ メッセージ
    3問目に既に1回不正解の状態
    不正解の場合
    ✓ 問題番号が変わらないこと
    ✓ 得点が変わらないこと
    ✓ 連続不正解数が増えていること
    ✓ 同じ問題を繰り返すこと
    ✓ メッセージ
    3問目に既に2回不正解の状態
    不正解の場合
    ✓ 問題番号がひとつ進むこと
    ✓ 得点が変わらないこと
    ✓ 連続不正解数がリセットされていること
    ✓ 次の問題に進むこと
    ✓ 次の問題を設定した後のメッセージ
    最終問題に初挑戦する状態
    正解した場合
    ✓ isFinished は true
    ✓ 問題番号が変わらないこと
    ✓ 得点が加点されていること
    ✓ 連続不正解数がリセットされていること
    ✓ 次の問題に進まないこと
    ✓ メッセージ
    不正解の場合
    ✓ isFinished は false
    ✓ 問題番号が変わらないこと
    ✓ 得点が変わらないこと
    ✓ 連続不正解数が増えていること
    ✓ 同じ問題を繰り返すこと
    ✓ メッセージ
    最終問題に2回不正解している状態
    正解した場合
    ✓ isFinished は true
    ✓ 問題番号が変わらないこと
    ✓ 得点が加点されていること
    ✓ 連続不正解数がリセットされていること
    ✓ 次の問題に進まないこと
    ✓ メッセージ
    不正解の場合
    ✓ isFinished は true
    ✓ 問題番号が変わらないこと
    ✓ 得点が変わらないこと
    ✓ 連続不正解数がリセットされていること
    ✓ 次の問題に進まないこと
    ✓ メッセージ
    50 passing (59ms)
    class Session {
    start (item) {
    this.advance = 1;
    this.score = 0;
    this.accumIncorrects = 0;
    this.item = item;
    }
    restore (attrs) {
    Object.assign(this, attrs);
    }
    setNextItem (item) {
    this.item = item;
    }
    message () {
    if (this.shouldRepeatSameQuestion()) {
    return this.resultMessage;
    }
    if (this.advance === 7) {
    return `${this.resultMessage}終わりです。あなたは${this.score}点でした。`;
    }
    if (this.advance === 1) {
    return `簡単なクイズをしましょう。${this.reprompt()}`;
    }
    return `${this.resultMessage}${this.reprompt()}`;
    }
    reprompt () {
    return `${this.advance}番。 ${this.item.q}`;
    }
    receive (answer) {
    this.shouldRepeat = false;
    if (this.item.a === answer) {
    this.resultMessage = 'そうです。 では';
    this.score += 1;
    this.accumIncorrects = 0;
    if (this.advance < 7) {
    this.advance += 1;
    }
    } else {
    this.accumIncorrects += 1;
    switch (this.accumIncorrects) {
    case 1:
    this.resultMessage = `${answer}? もう一度言ってください。${this.item.q}`;
    this.shouldRepeat = true;
    break;
    case 2:
    this.resultMessage = `私には「${answer}」と聞こえましたが、それは正しくありません。もう一度言ってください。${this.item.q}`;
    this.shouldRepeat = true;
    break;
    default:
    this.resultMessage = `ちがいます。正解は${this.item.a}です。 では`;
    this.accumIncorrects = 0;
    if (this.advance < 7) {
    this.advance += 1;
    }
    break;
    }
    }
    }
    isFinished () {
    return this.advance === 7 && !this.shouldRepeat;
    }
    shouldRepeatSameQuestion () {
    return this.shouldRepeat === true;
    }
    }
    module.exports = {
    Session
    };
    ϩδοΫΛ-BNCEB͔Β
    ͋Δఔ౓Ҡಈͨ͠

    View Slide

  69. QuizIntent: function () { // 初期状態
    var random = getNextItemIndex();
    this.attributes['itemIndex'] = random; // 配列番号をitemIndexに保存
    const session = new Session();
    session.start(questions[random]);
    this.attributes['advance'] = session.advance;
    this.attributes['score'] = session.score;
    this.attributes['accumIncorrects'] = session.accumIncorrects;
    this.emit(':ask', session.message(), session.reprompt());
    },
    ॳճىಈॲཧଆ
    まだロジックを切り出しただけ。
    Lambda レベルのテストのグリーンを保つため、
    リクエストをまたがるデータの持ち方は変えない

    View Slide

  70. AnswerIntent: function () { // ユーザからの返答により起動される
    // スロットから回答を取得
    var usersAnswer = this.event.request.intent.slots.Answer.value;
    var currentQuestion = questions[this.attributes['itemIndex']];
    const session = new Session();
    session.restore({
    advance: this.attributes['advance'],
    score: this.attributes['score'],
    accumIncorrects: this.attributes['accumIncorrects'],
    item: currentQuestion
    });
    session.receive(usersAnswer);
    this.attributes['advance'] = session.advance;
    this.attributes['score'] = session.score;
    this.attributes['accumIncorrects'] = session.accumIncorrects;
    if (session.shouldRepeatSameQuestion()) {
    this.emit(':ask', session.message(), session.reprompt());
    } else if (session.isFinished()) { // 全ての問題が終了した場合
    this.emit(':tell', session.message()); // 会話を終える
    } else { // 続きの問題がある場合
    var random = getNextItemIndex();
    this.attributes['itemIndex'] = random;
    session.setNextItem(questions[random]);
    this.emit(':ask', session.message(), session.reprompt()); // 会話を続ける
    }
    },
    ճ౴डཧଆ
    現在の状態や正答/誤答にもとづく
    メッセージがLambdaから消えた

    View Slide

  71. ·ͩ·ͩߦͧ͘

    View Slide

  72. wϞσϧͷ͍࣋ͬͯΔ৘ใΛϦΫΤετΛ·͍ͨͰμϯ
    ϓϦετΞͰ͖ΔΑ͏ʹͯ͠ɺ"MFYBݻ༗ͷػೳ
    BUUSJCVUFT
    ΁ͷґଘΛݮΒ͍ͯ͘͠
    w-BNCEBϨϕϧͷςετ͸Ұ࣌తʹ੺͘ͳΔ͕ɺظ଴
    ஋ʹμϯϓσʔλΛՃ͑ΔܗͰ྘ʹ໭͍ͯ͘͠
    wϞσϧΫϥε͸Ҿ͖ଓ͖ςετۦಈ։ൃͰ։ൃ͢Δ
    ؀ڥґଘΛݮΒ͍ͯ͘͠ઓུ

    View Slide

  73. describe('Session#dump()', () => {
    let session;
    beforeEach(() => {
    session = new Session();
    session.start({
    q: '広島県の県の花は?',
    a: 'モミジ',
    g: 'PrefectureFlower'
    });
    });
    it('dump', () => {
    assert.deepEqual(session.dump(), {
    advance: 1,
    score: 0,
    accumIncorrects: 0,
    item: {
    q: '広島県の県の花は?',
    a: 'モミジ',
    g: 'PrefectureFlower'
    }
    });
    });
    });
    3
    FE
    ·ͣ͸EVNQػೳͷςετΛॻ͍ͯ੺͘͢Δ

    View Slide

  74. class Session {
    start (item) {
    this.advance = 1;
    this.score = 0;
    this.accumIncorrects = 0;
    this.item = item;
    }
    dump () {
    return {
    advance: this.advance,
    score: this.score,
    accumIncorrects: this.accumIncorrects,
    item: this.item
    };
    }
    restore (attrs) {
    Object.assign(this, attrs);
    }
    (
    SFFO
    γϯϓϧʹάϦʔϯʹ͢Δ

    View Slide

  75. Ҏ͙߱Δ͙Δ
    3FE(SFFO3FGBDUPSͷ
    ճసΛଓ͚·͢
    初回の出題時
    ✓ 問題番号は 1
    ✓ 得点は 0
    ✓ 連続不正解数は 0
    ✓ 次の問題に進むこと
    ✓ メッセージ
    ✓ reprompt
    ✓ dump
    3問目に初挑戦する状態
    正解した場合
    ✓ 問題番号がひとつ進むこと
    ✓ 得点が加点されていること
    ✓ 連続不正解数がリセットされていること
    ✓ 次の問題に進むこと
    正解後に次の問題を設定した状態
    ✓ dump
    ✓ メッセージ
    不正解の場合
    ✓ 問題番号が変わらないこと
    ✓ 得点が変わらないこと
    ✓ 連続不正解数が増えていること
    ✓ 同じ問題を繰り返すこと
    ✓ メッセージ
    3問目に既に1回不正解の状態
    不正解の場合
    ✓ 問題番号が変わらないこと
    ✓ 得点が変わらないこと
    ✓ 連続不正解数が増えていること
    ✓ 同じ問題を繰り返すこと
    ✓ メッセージ
    3問目に既に2回不正解の状態
    不正解の場合
    ✓ 問題番号がひとつ進むこと
    ✓ 得点が変わらないこと
    ✓ 連続不正解数がリセットされていること
    ✓ 次の問題に進むこと
    不正解後に次の問題を設定した状態
    ✓ dump
    ✓ メッセージ
    最終問題に初挑戦する状態
    正解した場合
    ✓ isFinished は true
    ✓ 問題番号が変わらないこと
    ✓ 得点が加点されていること
    ✓ 連続不正解数がリセットされていること
    ✓ 次の問題に進まないこと
    ✓ メッセージ
    不正解の場合
    ✓ isFinished は false
    ✓ 問題番号が変わらないこと
    ✓ 得点が変わらないこと
    ✓ 連続不正解数が増えていること
    ✓ 同じ問題を繰り返すこと
    ✓ メッセージ
    最終問題に2回不正解している状態
    正解した場合
    ✓ isFinished は true
    ✓ 問題番号が変わらないこと
    ✓ 得点が加点されていること
    ✓ 連続不正解数がリセットされていること
    ✓ 次の問題に進まないこと
    ✓ メッセージ
    不正解の場合
    ✓ isFinished は true
    ✓ 問題番号が変わらないこと
    ✓ 得点が変わらないこと
    ✓ 連続不正解数がリセットされていること
    ✓ 次の問題に進まないこと
    ✓ メッセージ
    LaunchRequest を起動して最初の問題を出題
    ✓ 連続不正解数が0であること
    ✓ 返答の音声内容が初回の出題に伴う内容であること
    ✓ 進行状況は1であること
    ✓ 得点は0であること
    ✓ 出題された問題
    ✓ handler の response
    問題に正解した場合
    ✓ 連続不正解数が0に戻っていること
    ✓ 返答の音声内容が問題の正解に伴う内容であること
    ✓ 次の問題に進んでいるので進行状況が進んでいること
    ✓ 得点が1増えていること
    ✓ 次の問題に進んでいるので問題が変わっていること
    ✓ handler の response
    1回目の不正解の場合
    ✓ 連続不正解数が増えていること
    ✓ 返答の音声内容が1回目の不正解に伴う内容であること
    ✓ 進行状況が進んでいないこと
    ✓ 得点が変わらないこと
    ✓ 問題が変わらないこと
    ✓ handler の response
    2回目の不正解の場合
    ✓ 連続不正解数が増えていること
    ✓ 返答の音声内容が2回目の不正解に伴う内容であること
    ✓ 進行状況が進んでいないこと
    ✓ 得点が変わらないこと
    ✓ 問題が変わらないこと
    3回目の不正解の場合
    ✓ 連続不正解数が0に戻っていること
    ✓ 返答の音声内容が3回目の不正解に伴う内容であること
    ✓ 次の問題に進んでいるので進行状況が進んでいること
    ✓ 得点が変わらないこと
    ✓ 次の問題に進んでいるので問題が変わっていること
    最終問題に正解した場合
    ✓ shouldEndSession が true になっていること
    ✓ 連続不正解数が0に戻っていること
    ✓ 返答の音声内容がクイズ終了を知らせる内容であること
    ✓ 進行状況が変わらないこと
    ✓ 得点が1増えていること
    ✓ 問題が変わらないこと
    ✓ handler の response
    88 passing (82ms)

    View Slide

  76. QuizIntent: function () { // 初期状態
    const session = new Session();
    session.start(questions[getNextItemIndex()]);
    this.attributes['dump'] = session.dump();
    this.emit(':ask', session.message(), session.reprompt()); // 相手の回答を待つ
    },
    AnswerIntent: function () { // ユーザからの返答により起動される
    const usersAnswer = this.event.request.intent.slots.Answer.value;
    const session = new Session();
    session.restore(this.attributes['dump']);
    session.receive(usersAnswer);
    if (session.shouldRepeatSameQuestion()) {
    this.attributes['dump'] = session.dump();
    this.emit(':ask', session.message(), session.reprompt());
    } else if (session.isFinished()) { // 全ての問題が終了した場合
    this.attributes['dump'] = session.dump();
    this.emit(':tell', session.message()); // 会話を終える
    } else { // 続きの問題がある場合
    session.setNextItem(questions[getNextItemIndex()]);
    this.attributes['dump'] = session.dump();
    this.emit(':ask', session.message(), session.reprompt()); // 会話を続ける
    }
    },
    μϯϓ͚ͩͰϦΫΤετΛ·͍ͨͰਐΊΒΕΔΑ͏ʹͳͬͨ

    View Slide

  77. ·ͩ·ͩߦͧ͘

    View Slide

  78. ঢ়ଶભҠ΋Ϟσϧࣗ਎͕൑அ͢Δઓུ
    wঢ়ଶભҠ΋Ϟσϧࣗ਎͕൑அͰ͖ΔΑ͏ʹͯ͠ɺ
    -BNCEB΍"MFYBͱͷ઀஍໘ ݁߹౓
    ΛߋʹݮΒ͢
    w-BNCEBϨϕϧͷςετ͸ΦʔϧάϦʔϯͷ··૸
    Γଓ͚Δ
    wϞσϧΫϥε͸ςετۦಈ։ൃͰܧଓ։ൃ͢Δ

    View Slide

  79. class Session {
    constructor ({env}) {
    this.env = env;
    }
    start () {
    this.advance = 1;
    this.score = 0;
    this.accumIncorrects = 0;
    this.item = this.env.nextItem();
    }
    command () {
    return this.isFinished() ? ':tell' : ':ask';
    }
    receive (answer) {
    // 省略
    if (!this.shouldRepeatSameQuestion() && !this.isFinished()) { // 続きの問題がある場合
    this.item = this.env.nextItem();
    }
    }
    isFinished () {
    return this.advance === 7 && !this.shouldRepeat;
    }
    ࣍ͷ໰୊ʹߦ͔͘Ͳ͏͔΋Ϟσϧ͕ࣗ෼Ͱ൑அ͢Δ

    View Slide

  80. var createHandlers = function (getNextItemIndex) {
    const env = {
    nextItem () {
    return questions[getNextItemIndex()];
    }
    };
    return {
    QuizIntent: function () { // 初期状態
    const session = new Session({env});
    session.start();
    this.attributes['dump'] = session.dump();
    this.emit(session.command(), session.message(), session.reprompt());
    },
    AnswerIntent: function () { // ユーザからの返答により起動される
    const usersAnswer = this.event.request.intent.slots.Answer.value;
    const session = new Session({env});
    session.restore(this.attributes['dump']);
    session.receive(usersAnswer);
    this.attributes['dump'] = session.dump();
    this.emit(session.command(), session.message(), session.reprompt());
    },
    -BNCEBଆͷ੹຿͕ݮΓɺ͔ͳΓεοΩϦ͖ͯͨ͠

    View Slide

  81. ΋͏গ͠ߦͧ͘

    View Slide

  82. ઀஍໘Λ࠷খʹ͢Δઓུ
    wΫϥεΛެ։ͤͣɺϑΝΫτϦʔؔ਺͚ͩΛެ։
    ͢Δ͜ͱͰ಺෦ߏ଄Λ͞ΒʹӅṭ͠ɺ-BNCEB΍
    "MFYBͱͷ઀஍໘ ݁߹౓
    Λ࠷খʹ͢Δ
    w-BNCEBϨϕϧͷςετ͸ΦʔϧάϦʔϯͷ··
    ૸Γଓ͚Δ
    wϞσϧΫϥε͸ςετۦಈ։ൃͰܧଓ։ൃ͢Δ

    View Slide

  83. const startSession = ({env}) => {
    const session = new Session({env});
    session.start();
    return session;
    };
    const restoreSession = ({env, dump}) => {
    const session = new Session({env});
    session.restore(dump);
    return session;
    };
    module.exports = {
    startSession,
    restoreSession
    };
    Session クラスの export もやめる
    ந৅౓ͷߴ͍ؔ਺ͷΈެ։

    View Slide

  84. var questions = require('./questions.json');
    -const {Session} = require('./models');
    +const {startSession, restoreSession} = require('./models');
    var createHandlers = function (getNextItemIndex) {
    const env = {
    @@ -13,24 +13,21 @@ var createHandlers = function (getNextItemIndex) {
    return {
    QuizIntent: function () { // 初期状態
    - const session = new Session({env});
    - session.start();
    + const session = startSession({env});
    this.attributes['dump'] = session.dump();
    this.emit(session.command(), session.message(), session.reprompt());
    },
    AnswerIntent: function () { // ユーザからの返答により起動される
    const usersAnswer = this.event.request.intent.slots.Answer.value;
    - const session = new Session({env});
    - session.restore(this.attributes['dump']);
    + const session = restoreSession({env, dump: this.attributes['dump']});
    session.receive(usersAnswer);
    this.attributes['dump'] = session.dump();
    this.emit(session.command(), session.message(), session.reprompt());
    },
    ৘ใӅṭ͕ਐΉ

    View Slide

  85. const {startSession, restoreSession} = require('./models');
    QuizIntent: function () { // 初期状態
    const session = startSession({env});
    this.attributes['dump'] = session.dump();
    this.emit(session.command(), session.message(), session.reprompt());
    },
    AnswerIntent: function () { // ユーザからの返答により起動される
    const usersAnswer = this.event.request.intent.slots.Answer.value;
    const session = restoreSession({env, dump: this.attributes['dump']});
    session.receive(usersAnswer);
    this.attributes['dump'] = session.dump();
    this.emit(session.command(), session.message(), session.reprompt());
    },
    ج൫ͱͯ͠ͷ"MFYB-BNCEBͱϏδωεϩδοΫΛ෼཭Ͱ͖ͨ

    View Slide

  86. ͻͲ͔ͬͨͱ͖ͱൺ΂ͯΈΔ
    QuizIntent: function () { // 初期状態
    this.attributes['advance'] = 1; // 進行状況を初期化
    this.attributes['score'] = 0; // 得点を初期化
    this.attributes['accumIncorrects'] = 0; // 連続不正解数を初期化
    var random = getNextItemIndex();
    this.attributes['itemIndex'] = random; // 配列番号をitemIndexに保存
    var message = `簡単なクイズをしましょう。1番。 ${questions[random].q}`;
    var reprompt = `1番。 ${questions[random].q}`;
    this.emit(':ask', message, reprompt); // 相手の回答を待つ
    },
    AnswerIntent: function () { // ユーザからの返答により起動される
    // スロットから回答を取得
    var usersAnswer = this.event.request.intent.slots.Answer.value;
    var currentQuestion = questions[this.attributes['itemIndex']];
    var shouldRepeatSameQuestion = false;
    var resultMessage;
    if (currentQuestion.a === usersAnswer) { // 正解の場合
    resultMessage = 'そうです。 では';
    this.attributes['score']++;
    this.attributes['accumIncorrects'] = 0;
    } else { // 不正解の場合
    this.attributes['accumIncorrects']++;
    switch (this.attributes['accumIncorrects']) {
    case 1:
    resultMessage = `${usersAnswer}? もう一度言ってください。${currentQuestion.q}`;
    shouldRepeatSameQuestion = true;
    break;
    case 2:
    resultMessage = `私には「${usersAnswer}」と聞こえましたが、それは正しくありません。もう一度言ってください。${currentQuestion.q}`;
    shouldRepeatSameQuestion = true;
    break;
    default:
    resultMessage = `ちがいます。正解は${currentQuestion.a}です。 では`;
    this.attributes['accumIncorrects'] = 0;
    break;
    }
    }
    if (shouldRepeatSameQuestion) {
    var reprompt = `${this.attributes['advance']}番。 ${currentQuestion.q}`;
    this.emit(':ask', resultMessage, reprompt);
    } else if (this.attributes['advance'] < 7) { // 続きの問題がある場合
    this.attributes['advance']++;
    var random = getNextItemIndex();
    this.attributes['itemIndex'] = random;
    var reprompt = `${this.attributes['advance']}番。 ${questions[random].q}`;
    this.emit(':ask', resultMessage + reprompt, reprompt); // 会話を続ける
    } else { // 全ての問題が終了した場合
    var endMessage = `終わりです。あなたは${this.attributes['score']}点でした。`;
    this.emit(':tell', resultMessage + endMessage); // 会話を終える
    }
    },
    ͜ɺ͜Ε͸ʜʜ

    View Slide

  87. Agenda
    ݱঢ়֬ೝ
    ςελϏϦςΟΛ͚͋͜͡Δ
    ϞσϧΛ෼཭͢Δ
    ΞʔΩςΫνϟΛఆΊΔ

    View Slide

  88. ҆ఆґଘͷݪଇ
    ʮมԽ͠΍͍͢΋ͷʹґଘ͠ͳ͍ʯ

    View Slide

  89. IUUQTUIMJHIUDPNCMPHVODMFCPCUIFDMFBOBSDIJUFDUVSFIUNM
    ΞʔΩςΫνϟΛߟ͑Δ

    View Slide

  90. ͯ͞ɺօ͞Μ͸ʮ৘ใʯͱʮσʔλʯͷҧ͍Λ͝ଘ஌Ͱ͠ΐ͏͔ɻ
    զʑ͕ཉ͍͠ͷ͸ɺҙຯͷ͋Δʢ໨తΛ࣋ͬͨʣਖ਼͍͠৘ใͳͷͰ͢ɻ
    Ұํɺσʔλ͸୯ͳΔ֤छͷࣄ࣮ͷ஋ʢԿΒ͔ͷɺ໊শͱ͔೔෇ͱ͔ۚ
    ֹͱ͔ʣͰ͋ͬͯͦΕࣗମʹ໨త͸͋Γ·ͤΜɻ
    ໨తΛ࣋ͬͨ৘ใ͸ɺແ໨తͳࣄ࣮Λूੵͨ͠σʔλΛछʑՃ޻ͯ͠ಘ
    ΒΕ·͢ɻσʔλ͸།Ұແೋͷࣄ࣮஋Ͱ͔͢ΒɺͦΕ͔Β࡞Γग़͞ΕΔ
    ৘ใ͸ͲΕ΋͕ਖ਼͘͠ɺޓ͍ʹ੔߹ੑ͕ͱΕ͍ͯ·͢ɻ
    ᴷ࿨ాলೋ
    ʰ42-Ξϯνύλʔϯʱ؂༁ऀલॻ͖
    ʮ৘ใʯͱʮσʔλʯͷҧ͍Λҙࣝ͢Δ

    View Slide

  91. dump: {
    advance: 7,
    score: 5,
    accumIncorrects: 0,
    item: {
    a: '盛岡',
    g: 'PrefecturalOfficeLocation',
    q: '岩手県の県庁所在地は?'
    }
    }
    ͍ͭ͜͸ࣄ࣮ͩΖ͏͔ɺ৘ใͩΖ͏͔

    View Slide

  92. Block
    Session
    Question
    Attempt
    Attempt
    Attempt
    Attempt
    Attempt
    Attempt
    Attempt
    Attempt
    Attempt
    Question
    Question
    Block
    Block
    ࣄ࣮Λ֨ೲ͍ͯ͜͠͏

    View Slide

  93. "attributes": {
    "dump": {
    "startedAt": 1540553739000,
    "finishedAt": null,
    "blocks": [
    {
    "startedAt": 1540553739000,
    "finishedAt": 1540553825000,
    "genres": [
    "PrefectureFlower",
    "PrefecturalOfficeLocation",
    "Romanization"
    ],
    "questions": [
    {
    "item": {
    "q": "山形県の県の花は?",
    "a": "べにばな",
    "g": "PrefectureFlower"
    },
    "startedAt": 1540553752000,
    "finishedAt": 1540553778000,
    "result": "correct",
    "attempts": [
    {
    "startedAt": 1540553752000,
    "finishedAt": 1540553767000,
    "userAnswer": "サクラ",
    "result": "incorrect"
    },
    {
    "startedAt": 1540553767000,
    "finishedAt": 1540553778000,
    "userAnswer": "べにばな",
    ࣄ࣮Λ௥ه֨ೲ͍ͯ͘͠

    View Slide

  94. 事実を扱う
    情報を扱う
    基盤を扱う
    永続化を扱う ࠷ऴతͳΞʔΩςΫνϟ
    ࣄ࣮͔ΒܭࢉͰ͖Δಘ఺౳΍
    76*ͱͯ͠ͷϝοηʔδͳͲΛѻ͏

    View Slide

  95. 5%%͸ɺઃܭͷͻΒΊ͖͕ਖ਼͍͠ॠؒʹ๚ΕΔ͜ͱΛอ
    ূ͢Δ΋ͷͰ͸ͳ͍ɻ͔͠͠ɺࣗ৴Λ༩͑ͯ͘ΕΔςετ
    ͱ͖ͪΜͱखೖΕ͞Εͨίʔυ͸ɺͻΒΊ͖΁ͷඋ͑Ͱ͋
    Γɺ͍͟ͻΒΊ͍ͨͱ͖ʹɺͦΕΛ۩ݱԽ͢ΔͨΊͷඋ͑
    Ͱ΋͋Δɻ
    ᴷ,FOU#FDL
    ʰςετۦಈ։ൃʱୈষ

    View Slide

  96. ·ͱΊ
    w ςετ͸લઢج஍
    w ͕ͩςετΛॻ͍͚ͨͩͰ͸࣭͸্͕Βͳ͍
    w ֎ք΁ͷґଘΛ෼཭͠ɺ઀஍໘Λ࠷খʹ͢Δ
    Α͏ͳΞʔΩςΫνϟΛఆΊΔ
    w ࣄ࣮Λ֨ೲ͠ɺ৘ใΛ͔ͦ͜ΒऔΓग़͢
    ͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠

    View Slide