外部に依存したコードもテストで駆動する / 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

9f3a83db74bee75a64b5e6ed106a775c?s=128

Takuto Wada

October 31, 2018
Tweet

Transcript

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

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

  3. ࿨ా୎ਓ UXBEB U@XBEB UXBEB

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

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

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

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

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

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

  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(); }; Ҿ͖ܧ͍ͩίʔυ
  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 に入れたものが リクエストを跨いで引き継がれる
  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); // 会話を終える } }, ճ౴डཧଆ
  13. { "q": "茨城県の県庁所在地は?", "a": "水戸", "g": "PrefecturalOfficeLocation" }, { "q":

    "茨城県の県の花は?", "a": "バラ", "g": "PrefectureFlower" }, { "q": "茨城県のローマ字表記は?", "a": "ibaraki", "g": "Romanization" }, { "q": "茨城県の都道府県コード番号は?", "a": "8", "g": "PrefectureOrder" }, { "q": "栃木県の県庁所在地は?", "a": "宇都宮", "g": "PrefecturalOfficeLocation" }, ࣭໰σʔλ KTPO
  14. ͳ͓ɺςετίʔυ͸ແ͍

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

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

  17. ީิ͸ͭ w BMFYBTLJMMUFTUGSBNFXPSL w ߴػೳ͔ͭந৅Խ͞Ε͓ͯΓɺ͔Ώ͍ͱ͜Ζʹख͕ಧ͖ʹ͍͘ w &BTZࢦ޲ w BXTMBNCEBNPDLDPOUFYU w

    ػೳ͕গͳ͘ɺϨΠϠ͕ബ͍ w 4JNQMFࢦ޲ w ͜͜͸ϨΨγʔαόϯφɻ৘ใͷগͳ͍ઓ৔ʹ͸খ͘͞௚ަੑͷߴ͍ 4JNQMFͳಓ۩͕ཉ͍͠
  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ͰςετΛॻ͍ͯΈΔ
  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͸ݟΑ͏ݟ·ͶͰखͰ࡞Δ
  20. $ npm test > mocha --require intelli-espower-loader test LaunchRequest を起動して最初の問題を出題

    ✓ handler の response 1 passing (19ms) ( SFFO Α͠Α͠ಈͧ͘
  21. }); it('handler の response', () => { assert.deepEqual(speechResponse, { version:

    '1.0', response: { shouldEndSession: false, outputSpeech: { type: 'SSML', ssml: '<speak> 簡単なクイズをしましょう。1番。 青森県の県庁所在地は? </speak>' }, reprompt: { outputSpeech: { type: 'SSML', ssml: '<speak> 1番。 青森県の県庁所在地は? </speak>' } } }, sessionAttributes: { advance: 1, score: 0, itemIndex: 4 }, userAgent: 'ask-nodejs/1.0.25 Node/v8.10.0' }); }); }); VOEFpOFEͰ͸βϧͳͷͰ׬શҰகͷςετʹॻ͖׵͑Δ
  22. $ npm test > mocha --require intelli-espower-loader test LaunchRequest を起動して最初の問題を出題

    1) handler の response 0 passing (33ms) 1 failing  3 FE
  23. + expected - actual { "response": { "outputSpeech": { -

    "ssml": "<speak> 簡単なクイズをしましょう。1番。 福井県の都道府県コード番号は? </speak>" + "ssml": "<speak> 簡単なクイズをしましょう。1番。 青森県の県庁所在地は? </speak>" "type": "SSML" } "reprompt": { "outputSpeech": { - "ssml": "<speak> 1番。 福井県の都道府県コード番号は? </speak>" + "ssml": "<speak> 1番。 青森県の県庁所在地は? </speak>" "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 ग़ྗ͕ظ଴஋ͱ׬શҰக͠ͳ͍
  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
  25. ϨΨγʔίʔυͷδϨϯϚ lίʔυΛมߋ͢ΔͨΊʹ͸ςετΛ੔උ͢Δ ඞཁ͕͋Δɻଟ͘ͷ৔߹ɺςετΛ੔උ͢Δ ͨΊʹ͸ɺίʔυΛมߋ͢Δඞཁ͕͋Δz

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

  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(); }; ϥϯμϜੑΛ൐͏ؔ਺Λ֎͔Βࠩ͠ࠐΊΔΑ͏ʹ͢Δ 変更前 変更後
  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', 'すみません、わかりませんでした。終わります。'); ౉͞Εͨؔ਺Λ࢖͏Α͏ʹ ॱ࣍ॻ͖׵͑Δ
  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)); }); ςετ͔Β͸ϥϯμϜͰ͸ͳ͘ݻఆ஋Λฦؔ͢਺Λࠩ͠ࠐΉ
  30. $ npm test > mocha --require intelli-espower-loader test LaunchRequest を起動して最初の問題を出題

    ✓ handler の response 1 passing (19ms) ग़ྗ͕ظ଴஋ͱ׬શҰக͢ΔΑ͏ʹͳͬͨ
  31. $ npm test > mocha --require intelli-espower-loader test LaunchRequest を起動して最初の問題を出題

    ✓ handler の response 問題に正解した場合 ✓ handler の response 問題に不正解の場合 ✓ handler の response 3 passing (23ms) ( SFFO ·ͣ͸ॳճىಈ࣌ɺਖ਼ղɺෆਖ਼ղͷέʔεΛ੔උ
  32. ͔Ζ͏ͯ͡ಆ͏४උ͕੔ͬͨ

  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); // 会話を終える } }, ݱ࣌఺ͷίʔυओཁ෦
  34. ͜͜Ͱ࢓༷มߋͰ͢ wؒҧ͑ͯ΋ɺಉ͡໰୊Λճ·Ͱ܁Γฦ͍ͨ͠ wؒҧ͑ͨճ਺ʹԠͯ͡ϝοηʔδΛม͍͑ͨ wਖ਼ղͨ͠Β࣍ͷ໰୊ʹਐΉ wճؒҧ͑ͨΒෆਖ਼ղͱͯ࣍͠ͷ໰୊ʹਐΉ ͕ͩɺ͍·΍ࢲͨͪʹ͸ςετ͕͋Δʂ ΍͍ͬͯͧ͘ʂʂ

  35. ࣍ͷ໨ඪΛߟ͑Δ ͦͷ໨ඪΛࣔ͢ςετΛॻ͘ ͦͷςετΛ࣮ߦࣦͯ͠ഊͤ͞Δ 3FE  ໨తͷίʔυΛॻ͘ Ͱॻ͍ͨςετΛ੒ޭͤ͞Δ (SFFO  ςετ͕௨Δ··ͰϦϑΝΫλϦϯάΛߦ͏

    3FGBDUPS  ̍ʙΛ܁Γฦ͢ 5%%ͷαΠΫϧ
  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 ·ͣ͸ࣦഊ͢ΔςετΛॻ͘ テストコード内で事前状態を作る
  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 ςετͷࣦഊΛ֬ೝ͢Δ 完全一致のテストはしばらくスキップ
  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']++; ࣦഊ͍ͯ͠ΔςετΛ੒ޭͤ͞ΔͨΊͷ࠷খݶͷίʔυΛॻ͘
  39. $ npm test > mocha --require intelli-espower-loader test LaunchRequest を起動して最初の問題を出題

    ✓ handler の response 問題に正解した場合 ✓ handler の response 1回目の不正解の場合 ✓ 連続不正解数が増えていること - handler の response 3 passing (21ms) 1 pending ( SFFO ςετͷ੒ޭΛ֬ೝ͢Δ
  40. assert(speechResponse.sessionAttributes.accumIncorrects === 1); }); + it('返答の音声内容が1回目の不正解に伴う内容であること', () => { +

    assert(speechResponse.response.outputSpeech.ssml === '<speak> 久慈? もう一度言ってください。岩手県の県庁所在地は? </speak>'); + }); 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 ·ࣦͣഊ͢ΔςετΛॻ͘
  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 ੒ޭͤ͞ΔͨΊͷ࠷খݶͷίʔυΛॻ͘
  42. い。岩手県の県庁所在地は? </speak>'); }); + 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 ͷճసΛ܁Γฦ͢
  43. ( SFFO }); - it.skip('handler の response', () => {

    + it('handler の response', () => { const expected = { version: '1.0', response: { shouldEndSession: false, outputSpeech: { type: 'SSML', - ssml: '<speak> ちがいます。正解は盛岡です。 では3番。 青森県の県庁所在地は? </speak>' + ssml: '<speak> 久慈? もう一度言ってください。岩手県の県庁所在地は? </speak>' }, reprompt: { outputSpeech: { type: 'SSML', - ssml: '<speak> 3番。 青森県の県庁所在地は? </speak>' + ssml: '<speak> 2番。 岩手県の県庁所在地は? </speak>' } } }, sessionAttributes: { - advance: 3, + advance: 2, score: 1, - itemIndex: 4 + accumIncorrects: 1, + itemIndex: 8 }, 1回目の不正解の場合 ✓ 連続不正解数が増えていること ✓ 返答の音声内容が1回目の不正解に伴う内容であること ✓ 進行状況が進んでいないこと ✓ 得点が変わらないこと ✓ 問題番号が変わらないこと ✓ handler の response 8 passing (20ms) ׬શҰகͷςετ΋ άϦʔϯ෮ؼ
  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 === '<speak> 私には「久慈」と聞こえましたが、それは正しくありません。 もう一度言ってください。岩手県の県庁所在地は? </speak>'); }); 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 ͷճసΛ܁Γฦ͢
  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 ͷճసΛ܁Γฦ͢
  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 ͷճసΛ܁Γฦ͢
  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 === '<speak> ちが います。正解は盛岡です。 では3番。 青森県の県庁所在地は? </speak>'); }); 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
  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
  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)
  50. ͕ͩɺͪΐͬͱ଴ͬͯ΄͍͠

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

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

  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); // 会話を終える } }, ͜ɺ͜Ε͸ʜʜ
  54. ϓϩμΫτίʔυͷ࣭͸ Ή͠Ζ௿Լ͍ͯ͠ΔͷͰ͸ʜʜʁ

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

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

    w ࠷ॳͷϒϩοΫͰ͸δϟϯϧ͕ॏͳΒͳ͍Α͏ʹϥϯμ Ϝʹ໰ग़͍ͨ͠ ͑ͬɺ͍·ʜʜʁ
  57. ͞Βʹ௥͍ଧͪΛ͔͚ΔΑ͏ʹαϙʔτऴྃ npm WARN deprecated alexa-sdk@1.0.25: 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 ͑ͬɺ͍·ʜʜʁ
  58. ͜Μͳ͸ͣͰ͸ͳ͔ͬͨ

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

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

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

  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ʹґଘ͠ͳ͍Ϣχοτςετ͕ॻ͚Δ
  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͔ΒϩδοΫΛίϐʔͯ͠ςετΛ௨͢
  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ͷํ͔ΒϞσϧͷϩδοΫΛ࢖͏Α͏ʹมߋ
  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 ࣍͸ճ౴डཧΛςετϑΝʔετͰ
  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 γϯϓϧʹάϦʔϯʹ͢Δ
  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)
  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͔Β ͋Δఔ౓Ҡಈͨ͠
  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 レベルのテストのグリーンを保つため、 リクエストをまたがるデータの持ち方は変えない
  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から消えた
  71. ·ͩ·ͩߦͧ͘

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

  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ػೳͷςετΛॻ͍ͯ੺͘͢Δ
  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 γϯϓϧʹάϦʔϯʹ͢Δ
  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)
  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()); // 会話を続ける } }, μϯϓ͚ͩͰϦΫΤετΛ·͍ͨͰਐΊΒΕΔΑ͏ʹͳͬͨ
  77. ·ͩ·ͩߦͧ͘

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

  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; } ࣍ͷ໰୊ʹߦ͔͘Ͳ͏͔΋Ϟσϧ͕ࣗ෼Ͱ൑அ͢Δ
  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ଆͷ੹຿͕ݮΓɺ͔ͳΓεοΩϦ͖ͯͨ͠
  81. ΋͏গ͠ߦͧ͘

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

  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 もやめる ந৅౓ͷߴ͍ؔ਺ͷΈެ։
  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()); }, ৘ใӅṭ͕ਐΉ
  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ͱϏδωεϩδοΫΛ෼཭Ͱ͖ͨ
  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); // 会話を終える } }, ͜ɺ͜Ε͸ʜʜ
  87. Agenda ݱঢ়֬ೝ ςελϏϦςΟΛ͚͋͜͡Δ ϞσϧΛ෼཭͢Δ ΞʔΩςΫνϟΛఆΊΔ

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

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

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

  91. dump: { advance: 7, score: 5, accumIncorrects: 0, item: {

    a: '盛岡', g: 'PrefecturalOfficeLocation', q: '岩手県の県庁所在地は?' } } ͍ͭ͜͸ࣄ࣮ͩΖ͏͔ɺ৘ใͩΖ͏͔
  92. Block Session Question Attempt Attempt Attempt Attempt Attempt Attempt Attempt

    Attempt Attempt Question Question Block Block ࣄ࣮Λ֨ೲ͍ͯ͜͠͏
  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": "べにばな", ࣄ࣮Λ௥ه֨ೲ͍ͯ͘͠
  94. 事実を扱う 情報を扱う 基盤を扱う 永続化を扱う ࠷ऴతͳΞʔΩςΫνϟ ࣄ࣮͔ΒܭࢉͰ͖Δಘ఺౳΍ 76*ͱͯ͠ͷϝοηʔδͳͲΛѻ͏

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

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

    ͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠