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
7k
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
20220926_セキュリティチームの今_for_Drs._Prime_公開用.pdf
sota1235
0
39
再発防止策を考える技術 / #phpconsen
sota1235
10
3.5k
How to choose the best npm module for your team?
sota1235
9
450
Realtime Database for high traffic production application
sota1235
7
3.7k
Road to migrate JP Web as a microservice
sota1235
4
1.4k
インターフェース再入門 / Think Interface again
sota1235
6
10k
再発防止策を考える技術 #phpconfuk_rej
sota1235
1
1k
Update around Firebase #io18
sota1235
3
4.1k
Introduction for sonarwhal
sota1235
0
470
Other Decks in Technology
See All in Technology
ExaDB-D dbaascli で出来ること
oracle4engineer
PRO
0
1.8k
Exadata Database Service on Dedicated Infrastructure(ExaDB-D) UI スクリーン・キャプチャ集
oracle4engineer
PRO
0
1.1k
Server-Side Kotlin + Spring Boot + Exposedでやったこと
ikefukurou777
0
110
スケジュール指定のFargate Spotと友達になれた話
news_it_enj
0
240
マルチテナントの実現におけるDB設計とRLS / Utilizing RSL in multi-tenancy
soudai
20
5k
AIエージェントが変えるソフトウェアのパラダイム
masahiro_nishimi
0
200
マイクロサービスを横断したGoのコードレビュー
yuyu_hf
PRO
1
120
SmartHR プロダクトエンジニア求人ガイド 2024上期
smarthr
0
140
小さく始めるAnsible
stopendy
0
210
滑空スポーツ講習会2023 航空安全講習会 第4回 日常整備に役立ちそうな雑情報 / JSA Safety Seminar 2023 glider maintenance
jsaseminar
0
110
ECS on FargateへのSeekable OCI導入レポート
iwamot
0
260
第131回 雲勉【オンライン】AWSを9年触ってきて経験した落とし穴、ハマったポイント
iret
6
480
Featured
See All Featured
Automating Front-end Workflow
addyosmani
1353
200k
A Tale of Four Properties
chriscoyier
150
22k
CoffeeScript is Beautiful & I Never Want to Write Plain JavaScript Again
sstephenson
153
14k
It's Worth the Effort
3n
180
27k
Designing with Data
zakiwarfel
94
4.8k
The Psychology of Web Performance [Beyond Tellerrand 2023]
tammyeverts
4
1.4k
The Art of Programming - Codeland 2020
erikaheidi
40
12k
GitHub's CSS Performance
jonrohan
1023
450k
Building Adaptive Systems
keathley
29
1.8k
Art, The Web, and Tiny UX
lynnandtonic
288
19k
Become a Pro
speakerdeck
PRO
8
4.2k
Refactoring Trust on Your Teams (GOTO; Chicago 2020)
rmw
24
2.2k
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
ご清聴ありがとうございました