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
JavaScriptユニットテストの理想と現実
Search
Sota Sugiura
April 20, 2018
Technology
13
7.3k
JavaScriptユニットテストの理想と現実
Talk at 関西Node学園 梅田キャンパス 1時限目
https://nodejs.connpass.com/event/82614/
Sota Sugiura
April 20, 2018
Tweet
Share
More Decks by Sota Sugiura
See All by Sota Sugiura
内製したSlack Appで頑張るIncident Response@Waroom Meetup #1 / Incident Response with Slack App in 10X
sota1235
0
1.7k
20220926_セキュリティチームの今_for_Drs._Prime_公開用.pdf
sota1235
0
150
再発防止策を考える技術 / #phpconsen
sota1235
10
3.9k
How to choose the best npm module for your team?
sota1235
9
610
Realtime Database for high traffic production application
sota1235
7
4.1k
Road to migrate JP Web as a microservice
sota1235
4
1.7k
インターフェース再入門 / Think Interface again
sota1235
6
11k
再発防止策を考える技術 #phpconfuk_rej
sota1235
1
1.3k
Update around Firebase #io18
sota1235
3
4.4k
Other Decks in Technology
See All in Technology
今この時代に技術とどう向き合うべきか
gree_tech
PRO
2
2.1k
OSSで50の競合と戦うためにやったこと
yamadashy
3
890
Okta Identity Governanceで実現する最小権限の原則 / Implementing the Principle of Least Privilege with Okta Identity Governance
tatsumin39
0
150
AI時代、“平均値”ではいられない
uhyo
4
1.1k
「魔法少女まどか☆マギカ Magia Exedra」のIPのキャラクターを描くための3Dルック開発
gree_tech
PRO
0
130
Railsの話をしよう
yahonda
0
170
HonoとJSXを使って管理画面をサクッと型安全に作ろう
diggymo
0
150
Biz職でもDifyでできる! 「触らないAIワークフロー」を実現する方法
igarashikana
3
1.3k
混合雲環境整合異質工作流程工具運行關鍵業務 Job 的經驗分享
yaosiang
0
140
JSConf JPのwebsiteをGatsbyからNext.jsに移行した話 - Next.jsの多言語静的サイトと課題
leko
2
160
NLPコロキウム20251022_超効率化への挑戦: LLM 1bit量子化のロードマップ
yumaichikawa
1
170
Findy Team+ QAチーム これからのチャレンジ!
findy_eventslides
0
490
Featured
See All Featured
The Invisible Side of Design
smashingmag
302
51k
CSS Pre-Processors: Stylus, Less & Sass
bermonpainter
359
30k
Art, The Web, and Tiny UX
lynnandtonic
303
21k
Become a Pro
speakerdeck
PRO
29
5.6k
jQuery: Nuts, Bolts and Bling
dougneiner
65
7.9k
Side Projects
sachag
455
43k
BBQ
matthewcrist
89
9.8k
Gamification - CAS2011
davidbonilla
81
5.5k
A Tale of Four Properties
chriscoyier
161
23k
The Straight Up "How To Draw Better" Workshop
denniskardys
238
140k
No one is an island. Learnings from fostering a developers community.
thoeni
21
3.5k
Designing for humans not robots
tammielis
254
26k
Transcript
JavaScriptユニットテスト ⼊入⾨門 @sota1235 関⻄西Node学園 梅梅⽥田キャンパス1時限⽬目 2018/4/20
先週の⽇日曜⽇日 • 温泉⼊入りながら資料料作ってた • 考えれば考える程「ユニットテストの書き⽅方 はプロに任せたほうがいいのでは…?サバン ナの⼈人とか」と思い始めた • もっとフロントエンドの⼈人が苦しんでそうな ことは無いかと考えた
JavaScriptユニットテスト 理理想と現実 @sota1235 関⻄西Node学園 梅梅⽥田キャンパス1時限⽬目 2018/4/20
console.log(me) • Sota Sugiura(きりん) • @sota1235 • Mercari, Inc. •
将来の夢はJavaScriptに なることです
東京から来ました
今⽇日の話 • テストについて
None
今⽇日の話 • (いろんな恩恵を受けるために)テスト(を書きたい んだけど現実問題、古いコードベースとか設計も 何もないコードがあってソースがバンドルされて るわけでもない時、私達はどうするべきなのか)に ついて • 現実のつらみからテストを書ける状態に持ってい く話をします
• テストの書き⽅方はほとんど話しません
アジェンダ • 第1章 テストの必要性 • 第2章 現実との戦い • 第3章 モジュールを切り出す
第1章 テストの必要性
なぜテストは必要か • バグ防⽌止? • 負荷確認? • イレギュラー対策?
なぜテストは必要か • バグ防⽌止? • 負荷確認? • イレギュラー対策? • →品質担保のため
品質とは • 意図した通りに動作するか • きれいなコードか • 想定していない⼊入⼒力力に耐性があるか
ユニットテストとは • あるモジュールがある単⼀一の責務を果たして いるかをテストする • 最⼩小粒度でのテストを⾏行行う • モジュール単位での品質を担保する
例例えば ͤͶ • たこ焼きが美味しいと 思った時にクリックす る”せやね”ボタン • ボタンを押すとカウン トアップして押下済み
になる
ͤͶ 1. Clickされる 2. APIと通信する 3. 通信が返ったらボタンを押下 済みCSSにする 4.
数字をAPIレスポンスを元に 更更新する
ͤͶ 1. Clickされる 2. APIと通信する 3. 通信が返ったらボタンを押下 済みCSSにする 4.
数字をAPIレスポンスを元に 更更新する それぞれをテストする
責務ごとにテストを分けると • 1テストケースがシンプルになる • モジュールが責務ごとに分かれるよう強制さ れる
責務ごとにテストを分けると • 1テストケースがシンプルになる • モジュールが責務ごとに分かれるよう強制さ れる
なぜか
汚いコードはテストが書きづらい
汚いコードはテストが書きづらい =テストを書けるようにすると綺麗になりやすい
せやねボタン document.getElementById('.seyane-button').addEventListener('click', () => { fetch('https://test.com/seyane', { method: 'POST', })
.then(res => { if (res.ok) { document.getElementById('.seyane-button').style.color = 'gray'; } return res.json() }) .then({ count } => { document.getElementById('.seyane-counter').innerHTML = count; }); });
せやねボタン document.getElementById('.seyane-button').addEventListener('click', () => { fetch('https://test.com/seyane', { method: 'POST', })
.then(res => { if (res.ok) { document.getElementById('.seyane-button').style.color = 'gray'; } return res.json() }) .then({ count } => { document.getElementById('.seyane-counter').innerHTML = count; }); });
テスト書きづらい…(´・ω・`)
なぜ書きづらいか • このファイルをrequireしたら即実⾏行行される • イベントリスナに渡される1つのfunctionがいろ んなことしてる • 分岐の数の掛け算だけテストケースが増える • 不不確定要素が多い
• APIとの通信、DOM APIのコール
これが現実だ!!!! • ⼊入社して全てのフロントエンドのコードがキ レイって早々ないと思ってる • たぶん
第2章 現実との戦い
ターゲット • テストが書くのが難しく、またテスト⽂文化も そこまで浸透していない(主観)フロントエンド に着⽬目します
なぜテストが浸透していないか • フロントエンドのコードをモジュールでばら して書けるようになったのがここ数年年 • フロントエンドの要件⾃自体が複雑化した • 複雑化したロジックを保守する必要性が増し た 考察
フロントエンド is カオス • ステートフルな世界 • UIとロジックの2つの世界 • 様々な外部要因 •
APIとの通信、ローカルストレージ
現実 is カオス • N年年物のレガシーコード • Webpack?なにそれ美味しいの? • リポジトリに威⾵風堂々と居座るjQuery1.x.0
どう⽴立ち向かうのか • 既存のロジックにユニットテストを書く • 前にコストパフォーマンスを考える
テストは書いて終わりではない • 運⽤用、保守する必要がある • ロジックが変わればテストも書き換える • 品質を担保するためのものに品質を上げる時 間を取られてはいけない
コスパを考える • テスト対象のモジュールは変更更される可能性 が⾼高くないか • テスト対象はロジックでなくUIにまつわるも のでないか
例例えば • クリックされたら消費税を計算するロジック
例例えば • クリックされたら消費税を計算するロジック • 計算ロジックだけならテストしてよさそう
例例えば • クリックされたら消費税を計算するロジック • 計算ロジックだけならテストしてよさそう • jQueryで動的に⽣生成されるDOMのclass名
例例えば • クリックされたら消費税を計算するロジック • 計算ロジックだけならテストしてよさそう • jQueryで動的に⽣生成されるDOMのclass名 • いろんな都合でclass名変わる可能性が⾼高いしそも そもDOM構造も変わりやすい
テストを書く場所の勘所 • UIのテストは無駄になることが多い • E2Eテストが難しいと⾔言われる所以 • コアのロジックは変わることが少ない • 使い回せる粒度に保てば再利利⽤用性も上がる •
「社内npmライブラリとして使い回せるか」
第3章 モジュールを切り出す
モジュールを切り出す • 現実はだいたい多くの責務を持った「何か」 がそこにいる • この章ではその何かを実際にばらしあとはユ ニットテストを書くだけ、という状態にもっ ていく
せやねボタンreturns ͤͶ • たこ焼きが美味しいと 思った時にクリックす るせやねボタン • ボタンを押すとカウン トアップして押下済み
になる
せやねボタン document.getElementById('.seyane-button').addEventListener('click', () => { fetch('https://test.com/seyane', { method: 'POST', })
.then(res => { if (res.ok) { document.getElementById('.seyane-button').style.color = 'gray'; } return res.json() }) .then({ count } => { document.getElementById('.seyane-counter').innerHTML = count; }); });
ͤͶ 1. Clickされる 2. APIと通信する 3. 通信が返ったらボタンを押下 済みCSSにする 4.
数字をAPIレスポンスを元に 更更新する
ユーザインタラクションとロジックを 分離する
1. Clickされる 2. APIと通信する 3. 通信が返ったらボタンを押下 済みCSSにする 4. 数字をAPIレスポンスを元に 更更新する
Ǽ✣nj ƺȉǕǿDžǍǽȉ ȃǎǙDž
分離する function onSeyanaButtonClick() { fetch('https://test.com/seyane', { method: 'POST', }) .then(res
=> { if (res.ok) { document.getElementById('.seyane-button').style.color = 'gray'; } return res.json() }) .then({ count } => { document.getElementById('.seyane-counter').innerHTML = count; }); } document.getElementById('.seyane-button').addEventListener('click', onSeyanaButtonClick);
分離する function onSeyanaButtonClick() { fetch('https://test.com/seyane', { method: 'POST', }) .then(res
=> { if (res.ok) { document.getElementById('.seyane-button').style.color = 'gray'; } return res.json() }) .then({ count } => { document.getElementById('.seyane-counter').innerHTML = count; }); } document.getElementById('.seyane-button').addEventListener('click', onSeyanaButtonClick); Ǽ✣njƺȉǕǿDžǍǽȉ ȃǎǙDž
ロジックを分離する
1. Clickされる 2. APIと通信する 3. 通信が返ったらボタンを押下 済みCSSにする 4. 数字をAPIレスポンスを元に 更更新する
Ǽ✣nj ƺȉǕǿDžǍǽȉ ȃǎǙDž
ロジックの分離 function onSeyanaButtonClick() { fetch('https://test.com/seyane', { method: 'POST', }) .then(res
=> { if (res.ok) { document.getElementById('.seyane- button').style.color = 'gray'; } return res.json() }) .then({ count } => { document .getElementById(‘.seyane-counter') .innerHTML = count; }); } 1. Clickされる 2. APIと通信する 3. 通信が返ったらボタン を押下済みCSSにする 4. 数字をAPIレスポンス を元に更更新する
ロジックの分離 function onSeyanaButtonClick() { fetch('https://test.com/seyane', { method: 'POST', }) .then(res
=> { if (res.ok) { document.getElementById('.seyane- button').style.color = 'gray'; } return res.json() }) .then({ count } => { document .getElementById(‘.seyane-counter') .innerHTML = count; }); } 1. Clickされる 2. APIと通信する 3. 通信が返ったらボタン を押下済みCSSにする 4. 数字をAPIレスポンス を元に更更新する
ロジックの分離 /** * @param {Response} */ function changeSeyaneButtonStatus(res) { if
(res.ok) { document .getElementById(‘.seyane-button') .style.color = 'gray'; } } 1. Clickされる 2. APIと通信する 3. 通信が返ったらボタン を押下済みCSSにする 4. 数字をAPIレスポンス を元に更更新する
ロジックの分離 function onSeyanaButtonClick() { fetch('https://test.com/seyane', { method: 'POST', }) .then(res
=> { if (res.ok) { document.getElementById('.seyane- button').style.color = 'gray'; } return res.json() }) .then({ count } => { document .getElementById(‘.seyane-counter') .innerHTML = count; }); } 1. Clickされる 2. APIと通信する 3. 通信が返ったらボタン を押下済みCSSにする 4. 数字をAPIレスポンス を元に更更新する
ロジックの分離 /** * @param {Number} */ function updateSeyaneButtonCount(count) { document
.getElementById('.seyane-counter') .innerHTML = count; } 1. Clickされる 2. APIと通信する 3. 通信が返ったらボタン を押下済みCSSにする 4. 数字をAPIレスポンス を元に更更新する
ロジックの分離 function onSeyanaButtonClick() { fetch('https://test.com/seyane', { method: 'POST', }) .then(res
=> { changeSeyaneButtonStatus(res); return res.json() }) .then({ count } => { updateSeyaneButtonCount(count); }); } 1. Clickされる 2. APIと通信する 3. 通信が返ったらボタン を押下済みCSSにする 4. 数字をAPIレスポンス を元に更更新する
テスト書けそう
現実は泥泥臭臭い • 1つのイベントリスナに詰まっていたものをま ずイベントリスナとそれ以外で分けた • その後、ロジックから2つのロジックを切り出 した • 後はファイルを分けてテストを書くだけ!
つらい現実に⽴立ち向かうには • 今あるコードを紐解いていく • 紐解く時の鍵はUIとロジックの境⽬目 • 紐解いてfunction化、module化したものにテ ストを追加していく • 簡単なところから少しずつ、ばらしていく
余談: テストを書くという⽂文化 • もし今いるチームにテストを書く⽂文化が無い なら積極的にこのアプローチを試してほしい • 1つ簡単なサンプルがあるとみんな真似できる • 新しく追加するロジックでテストを書けるよ うになる
まとめ
まとめ • 現実はつらい • つらくてもテストには価値がある • テストを書くための審美眼を極める • UI, ロジックの境⽬目
• コスパの良さ
おまけ • 過去に社内向けにmochaのトレーニングリポ ジトリを作りました • 基礎的なテストを書けるようになりたい⽅方は どうぞ • TwitterなりIssueなりで質問待ってます
おまけ • この現実のつらさ を越えた先にある つらさについて過 去に発表したので よければどうぞ https://speakerdeck.com/sota1235/importwomotukusuruhua
ご清聴ありがとうございました