Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
外部に依存したコードもテストで駆動する / Test-Driven Architecture ...
Search
Takuto WADA
PRO
October 31, 2018
Programming
69k
98
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
外部に依存したコードもテストで駆動する / 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
More Decks by Takuto WADA
See All by Takuto WADA
負債のメタファと2026年 / Debt Metaphor in Agentic Engineering Age 202609 Edition
twada
PRO
12
6.7k
2026年のソフトウェア開発を考える(2026/07版) / Agentic Software Engineering 2026-07 Findy Edition
twada
PRO
40
33k
2026年のソフトウェア開発を考える(2026/05版) / Software Engineering Scrum Fest Niigata 2026 Edition
twada
PRO
28
19k
AI時代のTDD / TDD in AI Era 202604 Edition
twada
PRO
9
1.9k
ソフトウェアエンジニア人生サバイバルガイド / Software Engineers Survival Guide 202604 Edition
twada
PRO
31
23k
予防に勝る防御なし(2025年版) - 堅牢なコードを導く様々な設計のヒント / Growing Reliable Code PHP Conference Fukuoka 2025
twada
PRO
53
38k
SQLアンチパターン第2版 データベースプログラミングで陥りがちな失敗とその対策 / Intro to SQL Antipatterns 2nd
twada
PRO
47
46k
AI時代のソフトウェア開発を考える(2025/07版) / Agentic Software Engineering Findy 2025-07 Edition
twada
PRO
194
140k
技術選定の審美眼(2025年版) / Understanding the Spiral of Technologies 2025 edition
twada
PRO
120
120k
Other Decks in Programming
See All in Programming
JAWS-UG 東京支部が始める、JAWS-UG支部コラボ / JAWS-UG lunchtime LT Collaboration
y0hgi
0
110
iOSDC2026登壇資料.pdf
riofujimon
0
180
モジュールの視点からSwiftを読み解く #iosdc
s_shimotori
0
190
The Rails Doctrine Decade
koic
2
380
ゲームコントローラやキーボードのファームウェアをSwiftで書く
kishikawakatsumi
1
250
Workers Cache を知る
syumai
0
240
モデルのリファクタリングが難しいと思ったら、そもそも複雑だったのはビジネス仕様だった ? / is-the-business-domain-the-real-complexity
hatsu38
0
390
Snowflakeで業務アプリを作ろう。 Snowflakeのアプリ機能解説&実践ガイド
ayumu_yamaguchi
2
300
フロントエンドUIフレームワークのこれまでとこれから
ssssota
5
2.9k
更なる可用性を求めて、5年間運用したKotlinのアプリケーションをGoでリプレイスする話
ken_tunc
0
360
ハーネス設計入門 〜 基礎知識の整理から実務へのステップアップ 〜
kinopeee
18
15k
App Storeの外へ──日本のiOSサイドローディング入門 for iOSDC Japan 2026
yuukiw00w
0
240
Featured
See All Featured
The AI Search Optimization Roadmap by Aleyda Solis
aleyda
1
6.2k
Fashionably flexible responsive web design (full day workshop)
malarkey
409
67k
SEO Brein meetup: CTRL+C is not how to scale international SEO
lindahogenes
2
2.9k
Cheating the UX When There Is Nothing More to Optimize - PixelPioneers
stephaniewalter
287
14k
"I'm Feeling Lucky" - Building Great Search Experiences for Today's Users (#IAC19)
danielanewman
230
23k
Getting science done with accelerated Python computing platforms
jacobtomlinson
2
480
ReactJS: Keep Simple. Everything can be a component!
pedronauck
666
130k
Have SEOs Ruined the Internet? - User Awareness of SEO in 2025
akashhashmi
0
500
Effective software design: The role of men in debugging patriarchy in IT @ Voxxed Days AMS
baasie
1
540
The agentic SEO stack - context over prompts
schlessera
0
940
コードの90%をAIが書く世界で何が待っているのか / What awaits us in a world where 90% of the code is written by AI
rkaga
63
46k
Building a A Zero-Code AI SEO Workflow
portentint
PRO
0
730
Transcript
֎෦ʹґଘͨ͠ίʔυ ςετͰۦಈ͢Δ ాਓ !U@XBEB 0DU !"84%FW%BZ5PLZP
#AWSDevDay #AWSTDD ࡱӨ0,ʢͨͩ͠ɺγϟολʔԻΛ߇͑Ίʹʣ ࢿྉө૾ެ։͋Γ ࣮گେܴͰ͢ これらは小文字も可
ాਓ UXBEB U@XBEB UXBEB
Agenda ݱঢ়֬ೝ ςελϏϦςΟΛ͚͋͜͡Δ ϞσϧΛ͢Δ ΞʔΩςΫνϟΛఆΊΔ
γφϦΦಓݝΫΠζΛߦ͏ খ͍͞"MFYB4LJMM։ൃΛҾ͖ܧ͍ͩ
ΞϨΫαɺಓݝΫΠζΛ։͍ͯ ؆୯ͳΫΠζΛ͠·͠ΐ͏ɻ൪ɻ ಢݝͷݝிॴࡏʁ
Ӊٶʂ ؆୯ͳΫΠζΛ͠·͠ΐ͏ɻ൪ɻ ಢݝͷݝிॴࡏʁ ͦ͏Ͱ͢ɻͰ൪ɻ ԭೄݝͷݝͷՖʁ
͕͍ͪ·͢ɻਖ਼ղσΠΰͰ͢ɻ Ͱ൪ɻἚݝͷϩʔϚࣈදهʁ ͦ͏Ͱ͢ɻͰ൪ɻ ԭೄݝͷݝͷՖʁ ϋΠϏεΧεʁ
ͦ͏Ͱ͢ɻͰऴΘΓͰ͢ɻ ͋ͳͨͰͨ͠ɻ ਆށʂ ɾ ɾ ɾ ͦ͏Ͱ͢ɻͰ൪ɻ ฌݿݝͷݝிॴࡏʁ
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(); }; Ҿ͖ܧ͍ͩίʔυ
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 に入れたものが リクエストを跨いで引き継がれる
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); // 会話を終える } }, ճडཧଆ
{ "q": "茨城県の県庁所在地は?", "a": "水戸", "g": "PrefecturalOfficeLocation" }, { "q":
"茨城県の県の花は?", "a": "バラ", "g": "PrefectureFlower" }, { "q": "茨城県のローマ字表記は?", "a": "ibaraki", "g": "Romanization" }, { "q": "茨城県の都道府県コード番号は?", "a": "8", "g": "PrefectureOrder" }, { "q": "栃木県の県庁所在地は?", "a": "宇都宮", "g": "PrefecturalOfficeLocation" }, ࣭σʔλ KTPO
ͳ͓ɺςετίʔυແ͍
Agenda ݱঢ়֬ೝ ςελϏϦςΟΛ͚͋͜͡Δ ϞσϧΛ͢Δ ΞʔΩςΫνϟΛఆΊΔ
લઢج͕ཉ͍͠ wςετ͕ͳ͍ͱ҆શͳมߋͱࠓޙͷ։ൃܧଓ ͕͍͠ wཏੑ͍Βͳ͍ɻఱؾͳਖ਼ৗܥ )BQQZ 1BUI Ͱ͍͍ͷͰɺಈ͘ςετ͕ཉ͍͠ w·ͣςετΛಈ͔͢ͱ͜Ζ·Ͱ͍͖࣋ͬͯ ͍ͨ
ީิͭ w BMFYBTLJMMUFTUGSBNFXPSL w ߴػೳ͔ͭநԽ͞Ε͓ͯΓɺ͔Ώ͍ͱ͜Ζʹख͕ಧ͖ʹ͍͘ w &BTZࢦ w BXTMBNCEBNPDLDPOUFYU w
ػೳ͕গͳ͘ɺϨΠϠ͕ബ͍ w 4JNQMFࢦ w ͜͜ϨΨγʔαόϯφɻใͷগͳ͍ઓʹখ͘͞ަੑͷߴ͍ 4JNQMFͳಓ۩͕ཉ͍͠
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ͰςετΛॻ͍ͯΈΔ
{ "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ݟΑ͏ݟ·ͶͰखͰ࡞Δ
$ npm test > mocha --require intelli-espower-loader test LaunchRequest を起動して最初の問題を出題
✓ handler の response 1 passing (19ms) ( SFFO Α͠Α͠ಈͧ͘
}); 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ͰβϧͳͷͰશҰகͷςετʹॻ͖͑Δ
$ npm test > mocha --require intelli-espower-loader test LaunchRequest を起動して最初の問題を出題
1) handler の response 0 passing (33ms) 1 failing 3 FE
+ 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 ग़ྗ͕ظͱશҰக͠ͳ͍
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
ϨΨγʔίʔυͷδϨϯϚ lίʔυΛมߋ͢ΔͨΊʹςετΛඋ͢Δ ඞཁ͕͋Δɻଟ͘ͷ߹ɺςετΛඋ͢Δ ͨΊʹɺίʔυΛมߋ͢Δඞཁ͕͋Δz
߹෦ʢ4FBNʣ l߹෦ʢ4FBNʣͱɺͦͷॴΛฤू ͠ͳͯ͘ɺϓϩάϥϜͷৼΔ͍Λม͑Δ ͜ͱͷͰ͖ΔॴͰ͋Δz
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(); }; ϥϯμϜੑΛ͏ؔΛ֎͔Βࠩ͠ࠐΊΔΑ͏ʹ͢Δ 変更前 変更後
-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', 'すみません、わかりませんでした。終わります。'); ͞ΕͨؔΛ͏Α͏ʹ ॱ࣍ॻ͖͑Δ
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)); }); ςετ͔ΒϥϯμϜͰͳ͘ݻఆΛฦؔ͢Λࠩ͠ࠐΉ
$ npm test > mocha --require intelli-espower-loader test LaunchRequest を起動して最初の問題を出題
✓ handler の response 1 passing (19ms) ग़ྗ͕ظͱશҰக͢ΔΑ͏ʹͳͬͨ
$ npm test > mocha --require intelli-espower-loader test LaunchRequest を起動して最初の問題を出題
✓ handler の response 問題に正解した場合 ✓ handler の response 問題に不正解の場合 ✓ handler の response 3 passing (23ms) ( SFFO ·ͣॳճىಈ࣌ɺਖ਼ղɺෆਖ਼ղͷέʔεΛඋ
͔Ζ͏ͯ͡ಆ͏४උ͕ͬͨ
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); // 会話を終える } }, ݱ࣌ͷίʔυओཁ෦
͜͜Ͱ༷มߋͰ͢ wؒҧ͑ͯɺಉ͡Λճ·Ͱ܁Γฦ͍ͨ͠ wؒҧ͑ͨճʹԠͯ͡ϝοηʔδΛม͍͑ͨ wਖ਼ղͨ͠Β࣍ͷʹਐΉ wճؒҧ͑ͨΒෆਖ਼ղͱͯ࣍͠ͷʹਐΉ ͕ͩɺ͍·ࢲͨͪʹςετ͕͋Δʂ ͍ͬͯͧ͘ʂʂ
࣍ͷඪΛߟ͑Δ ͦͷඪΛࣔ͢ςετΛॻ͘ ͦͷςετΛ࣮ߦࣦͯ͠ഊͤ͞Δ 3FE తͷίʔυΛॻ͘ Ͱॻ͍ͨςετΛޭͤ͞Δ (SFFO ςετ͕௨Δ··ͰϦϑΝΫλϦϯάΛߦ͏
3FGBDUPS ̍ʙΛ܁Γฦ͢ 5%%ͷαΠΫϧ
-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 ·ࣦͣഊ͢ΔςετΛॻ͘ テストコード内で事前状態を作る
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 ςετͷࣦഊΛ֬ೝ͢Δ 完全一致のテストはしばらくスキップ
+ 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']++; ࣦഊ͍ͯ͠ΔςετΛޭͤ͞ΔͨΊͷ࠷খݶͷίʔυΛॻ͘
$ npm test > mocha --require intelli-espower-loader test LaunchRequest を起動して最初の問題を出題
✓ handler の response 問題に正解した場合 ✓ handler の response 1回目の不正解の場合 ✓ 連続不正解数が増えていること - handler の response 3 passing (21ms) 1 pending ( SFFO ςετͷޭΛ֬ೝ͢Δ
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 ·ࣦͣഊ͢ΔςετΛॻ͘
$ 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 ޭͤ͞ΔͨΊͷ࠷খݶͷίʔυΛॻ͘
い。岩手県の県庁所在地は? </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 ͷճసΛ܁Γฦ͢
( 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) શҰகͷςετ άϦʔϯ෮ؼ
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 ͷճసΛ܁Γฦ͢
--- 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 ͷճసΛ܁Γฦ͢
--- 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 ͷճసΛ܁Γฦ͢
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
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
$ 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)
͕ͩɺͪΐͬͱͬͯ΄͍͠
ઃܭྑ͘ͳ͍ͬͯΔͩΖ͏͔ʁ IUUQTUXPQBHJMFFTNDPKQXIBUEPXFOFFEGPSHSPXUIPGGVUVSFDCBGF رബԽͨ͠5%%ɺϓϩμΫτͷͷͨΊʹඞཁͳͷʁʙʰ݈શͳϏδωεͷܧଓతͷͨΊʹ݈શͳίʔυ͕ඞཁͩʱରஊʢ̒ʣΑΓ
ઃܭྑ͘ͳ͍ͬͯΔͩΖ͏͔ʁ IUUQTUXPQBHJMFFTNDPKQXIBUEPXFOFFEGPSHSPXUIPGGVUVSFDCBGF رബԽͨ͠5%%ɺϓϩμΫτͷͷͨΊʹඞཁͳͷʁʙʰ݈શͳϏδωεͷܧଓతͷͨΊʹ݈શͳίʔυ͕ඞཁͩʱରஊʢ̒ʣΑΓ
ݱ࣌ͷίʔυΛݟͯΈΔ 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); // 会話を終える } }, ͜ɺ͜Εʜʜ
ϓϩμΫτίʔυͷ࣭ Ή͠ΖԼ͍ͯ͠ΔͷͰʜʜʁ
lςετͰ্࣭͕Βͳ͍Ͱ͢Αɻ ςετ͋͘·Ͱ࣭Λ͋͛Δ͖͔ͬ ͚ɻ࣭Λ͋͛ΔͷϓϩάϥϛϯάͰ ͢ɻ͜Εେੲ͔Βͦ͏ɻz
͜͜Ͱ͍ଧͪΛ͔͚ΔΑ͏ʹ༷มߋ w ΛϒϩοΫʢ ʣʹ۠ΓɺલͷϒϩοΫͰ ؒҧ͑ͨδϟϯϧ ݝிॴࡏ Λ༏ઌͯ͠ग़͍ͨ͠ w ϒϩοΫͰಉ͡δϟϯϧग़͞ͳ͍
w ࠷ॳͷϒϩοΫͰδϟϯϧ͕ॏͳΒͳ͍Α͏ʹϥϯμ Ϝʹग़͍ͨ͠ ͑ͬɺ͍·ʜʜʁ
͞Βʹ͍ଧͪΛ͔͚ΔΑ͏ʹαϙʔτऴྃ 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 ͑ͬɺ͍·ʜʜʁ
͜ΜͳͣͰͳ͔ͬͨ
ݱঢ়ͷͭΒΈ wͭͷมߋཁҼʢج൫ͷࣄɺ༷ͷมߋʣ͕୯Ұͷ ϓϩμΫτίʔυʹͯ߱͢Γ͔͔ͬͯ͘Δ wج൫ͷΞοϓσʔτͱ༷มߋɺͪΖΜͲͪ ΒΒͳ͚ΕͳΒͳ͍ wݱঢ়ΫϦοΫϋϯυϥʹϏδωεϩδοΫ͕ॻ͔Ε ͍ͯΔঢ়ଶʹ͍͠ɻ͜ͷ··Ͱઌ͕ͳ͍ wϏδωεϩδοΫΛ"MFYB -BNCEB͔ΒҾ͖͕ͦ͏
Agenda ݱঢ়֬ೝ ςελϏϦςΟΛ͚͋͜͡Δ ϞσϧΛ͢Δ ΞʔΩςΫνϟΛఆΊΔ
ϞσϧΫϥεΛ͢Δઓུ w͜͜·Ͱॻ͍͖ͯͨ-BNCEBϨϕϧͷςετΛά Ϧʔϯʹอͪͳ͕ΒɺϩδοΫΛϞσϧΫϥεʹҾ ͖͕͍ͯ͘͠ wϞσϧΫϥεςετۦಈ։ൃͰ৽ن։ൃ͢Δ
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ʹґଘ͠ͳ͍Ϣχοτςετ͕ॻ͚Δ
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͔ΒϩδοΫΛίϐʔͯ͠ςετΛ௨͢
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ͷํ͔ΒϞσϧͷϩδοΫΛ͏Α͏ʹมߋ
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 ࣍ճडཧΛςετϑΝʔετͰ
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 γϯϓϧʹάϦʔϯʹ͢Δ
Ҏ͙߱Δ͙Δ 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)
$ 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͔Β ͋ΔఔҠಈͨ͠
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 レベルのテストのグリーンを保つため、 リクエストをまたがるデータの持ち方は変えない
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から消えた
·ͩ·ͩߦͧ͘
wϞσϧͷ͍࣋ͬͯΔใΛϦΫΤετΛ·͍ͨͰμϯ ϓϦετΞͰ͖ΔΑ͏ʹͯ͠ɺ"MFYBݻ༗ͷػೳ BUUSJCVUFT ͷґଘΛݮΒ͍ͯ͘͠ w-BNCEBϨϕϧͷςετҰ࣌తʹ͘ͳΔ͕ɺظ ʹμϯϓσʔλΛՃ͑ΔܗͰʹ͍ͯ͘͠ wϞσϧΫϥεҾ͖ଓ͖ςετۦಈ։ൃͰ։ൃ͢Δ ڥґଘΛݮΒ͍ͯ͘͠ઓུ
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ػೳͷςετΛॻ͍ͯ͘͢Δ
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 γϯϓϧʹάϦʔϯʹ͢Δ
Ҏ͙߱Δ͙Δ 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)
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()); // 会話を続ける } }, μϯϓ͚ͩͰϦΫΤετΛ·͍ͨͰਐΊΒΕΔΑ͏ʹͳͬͨ
·ͩ·ͩߦͧ͘
ঢ়ଶભҠϞσϧ͕ࣗஅ͢Δઓུ wঢ়ଶભҠϞσϧ͕ࣗஅͰ͖ΔΑ͏ʹͯ͠ɺ -BNCEB"MFYBͱͷ໘ ݁߹ ΛߋʹݮΒ͢ w-BNCEBϨϕϧͷςετΦʔϧάϦʔϯͷ·· Γଓ͚Δ wϞσϧΫϥεςετۦಈ։ൃͰܧଓ։ൃ͢Δ
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; } ࣍ͷʹߦ͔͘Ͳ͏͔Ϟσϧ͕ࣗͰஅ͢Δ
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ଆͷ͕ݮΓɺ͔ͳΓεοΩϦ͖ͯͨ͠
͏গ͠ߦͧ͘
໘Λ࠷খʹ͢Δઓུ wΫϥεΛެ։ͤͣɺϑΝΫτϦʔ͚ؔͩΛެ։ ͢Δ͜ͱͰ෦ߏΛ͞ΒʹӅṭ͠ɺ-BNCEB "MFYBͱͷ໘ ݁߹ Λ࠷খʹ͢Δ w-BNCEBϨϕϧͷςετΦʔϧάϦʔϯͷ·· Γଓ͚Δ wϞσϧΫϥεςετۦಈ։ൃͰܧଓ։ൃ͢Δ
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 もやめる நͷߴ͍ؔͷΈެ։
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()); }, ใӅṭ͕ਐΉ
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ͱϏδωεϩδοΫΛͰ͖ͨ
ͻͲ͔ͬͨͱ͖ͱൺͯΈΔ 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); // 会話を終える } }, ͜ɺ͜Εʜʜ
Agenda ݱঢ়֬ೝ ςελϏϦςΟΛ͚͋͜͡Δ ϞσϧΛ͢Δ ΞʔΩςΫνϟΛఆΊΔ
҆ఆґଘͷݪଇ ʮมԽ͍͢͠ͷʹґଘ͠ͳ͍ʯ
IUUQTUIMJHIUDPNCMPHVODMFCPCUIFDMFBOBSDIJUFDUVSFIUNM ΞʔΩςΫνϟΛߟ͑Δ
ͯ͞ɺօ͞Μʮใʯͱʮσʔλʯͷҧ͍Λ͝ଘͰ͠ΐ͏͔ɻ զʑ͕ཉ͍͠ͷɺҙຯͷ͋ΔʢతΛ࣋ͬͨʣਖ਼͍͠ใͳͷͰ͢ɻ Ұํɺσʔλ୯ͳΔ֤छͷࣄ࣮ͷʢԿΒ͔ͷɺ໊শͱ͔ͱ͔ۚ ֹͱ͔ʣͰ͋ͬͯͦΕࣗମʹత͋Γ·ͤΜɻ తΛ࣋ͬͨใɺແతͳࣄ࣮Λूੵͨ͠σʔλΛछʑՃͯ͠ಘ ΒΕ·͢ɻσʔλ།Ұແೋͷࣄ࣮Ͱ͔͢ΒɺͦΕ͔Β࡞Γग़͞ΕΔ ใͲΕ͕ਖ਼͘͠ɺޓ͍ʹ߹ੑ͕ͱΕ͍ͯ·͢ɻ ᴷాলೋ ʰ42-Ξϯνύλʔϯʱ༁ऀલॻ͖ ʮใʯͱʮσʔλʯͷҧ͍Λҙࣝ͢Δ
dump: { advance: 7, score: 5, accumIncorrects: 0, item: {
a: '盛岡', g: 'PrefecturalOfficeLocation', q: '岩手県の県庁所在地は?' } } ͍ͭ͜ࣄ࣮ͩΖ͏͔ɺใͩΖ͏͔
Block Session Question Attempt Attempt Attempt Attempt Attempt Attempt Attempt
Attempt Attempt Question Question Block Block ࣄ࣮Λ֨ೲ͍ͯ͜͠͏
"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": "べにばな", ࣄ࣮Λه֨ೲ͍ͯ͘͠
事実を扱う 情報を扱う 基盤を扱う 永続化を扱う ࠷ऴతͳΞʔΩςΫνϟ ࣄ࣮͔ΒܭࢉͰ͖Δಘ 76*ͱͯ͠ͷϝοηʔδͳͲΛѻ͏
5%%ɺઃܭͷͻΒΊ͖͕ਖ਼͍͠ॠؒʹ๚ΕΔ͜ͱΛอ ূ͢ΔͷͰͳ͍ɻ͔͠͠ɺࣗ৴Λ༩͑ͯ͘ΕΔςετ ͱ͖ͪΜͱखೖΕ͞ΕͨίʔυɺͻΒΊ͖ͷඋ͑Ͱ͋ Γɺ͍͟ͻΒΊ͍ͨͱ͖ʹɺͦΕΛ۩ݱԽ͢ΔͨΊͷඋ͑ Ͱ͋Δɻ ᴷ,FOU#FDL ʰςετۦಈ։ൃʱୈষ
·ͱΊ w ςετલઢج w ͕ͩςετΛॻ͍͚ͨͩͰ্࣭͕Βͳ͍ w ֎քͷґଘΛ͠ɺ໘Λ࠷খʹ͢Δ Α͏ͳΞʔΩςΫνϟΛఆΊΔ w ࣄ࣮Λ֨ೲ͠ɺใΛ͔ͦ͜ΒऔΓग़͢
͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠