Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
外部に依存したコードもテストで駆動する / Test-Driven Architecture ...
Search
Takuto Wada
PRO
October 31, 2018
Programming
98
68k
外部に依存したコードもテストで駆動する / 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
Share
More Decks by Takuto Wada
See All by Takuto Wada
予防に勝る防御なし(2025年版) - 堅牢なコードを導く様々な設計のヒント / Growing Reliable Code PHP Conference Fukuoka 2025
twada
PRO
33
10k
SQLアンチパターン第2版 データベースプログラミングで陥りがちな失敗とその対策 / Intro to SQL Antipatterns 2nd
twada
PRO
43
23k
AI時代のソフトウェア開発を考える(2025/07版) / Agentic Software Engineering Findy 2025-07 Edition
twada
PRO
173
88k
技術選定の審美眼(2025年版) / Understanding the Spiral of Technologies 2025 edition
twada
PRO
114
89k
The Clean ArchitectureがWebフロントエンドでしっくりこないのは何故か / Why The Clean Architecture does not fit with Web Frontend
twada
PRO
83
35k
組織に自動テストを書く文化を根付かせる戦略(2024冬版) / Building Automated Test Culture 2024 Winter Edition
twada
PRO
36
13k
ピラミッド、アイスクリームコーン、SMURF: 自動テストの最適バランスを求めて / Pyramid Ice-Cream-Cone and SMURF
twada
PRO
10
2.8k
組織に自動テストを書く文化を根付かせる戦略(2024秋版) / Building Automated Test Culture 2024 Autumn Edition
twada
PRO
14
7.2k
これまでと違う学び方をしたら挫折せずにRustを学べた話 / Programming Rust techramen24conf LT
twada
PRO
34
29k
Other Decks in Programming
See All in Programming
三者三様 宣言的UI
kkagurazaka
0
370
なんでRustの環境構築してないのにRust製のツールが動くの? / Why Do Rust-Based Tools Run Without a Rust Environment?
ssssota
15
48k
CSC509 Lecture 10
javiergs
PRO
0
170
CSC509 Lecture 09
javiergs
PRO
0
290
SODA - FACT BOOK(JP)
sodainc
1
9.3k
Module Proxyのマニアックな話 / Niche Topics in Module Proxy
kuro_kurorrr
0
2.6k
HTTPじゃ遅すぎる! SwitchBotを自作ハブで動かして学ぶBLE通信
occhi
0
220
Swift Concurrency 年表クイズ
omochi
3
220
Migration to Signals, Resource API, and NgRx Signal Store
manfredsteyer
PRO
0
160
自動テストのアーキテクチャとその理由ー大規模ゲーム開発の場合ー
segadevtech
2
840
CSC509 Lecture 11
javiergs
PRO
0
300
Blazing Fast UI Development with Compose Hot Reload (Bangladesh KUG, October 2025)
zsmb
2
490
Featured
See All Featured
Git: the NoSQL Database
bkeepers
PRO
431
66k
Fantastic passwords and where to find them - at NoRuKo
philnash
52
3.5k
RailsConf & Balkan Ruby 2019: The Past, Present, and Future of Rails at GitHub
eileencodes
140
34k
How To Stay Up To Date on Web Technology
chriscoyier
791
250k
Raft: Consensus for Rubyists
vanstee
140
7.2k
Keith and Marios Guide to Fast Websites
keithpitt
413
23k
Rails Girls Zürich Keynote
gr2m
95
14k
Chrome DevTools: State of the Union 2024 - Debugging React & Beyond
addyosmani
9
970
Site-Speed That Sticks
csswizardry
13
960
10 Git Anti Patterns You Should be Aware of
lemiorhan
PRO
658
61k
Unsuck your backbone
ammeep
671
58k
Documentation Writing (for coders)
carmenintech
76
5.1k
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 ࣄ࣮Λ֨ೲ͠ɺใΛ͔ͦ͜ΒऔΓग़͢
͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠