Talk at 関西Node学園 梅田キャンパス 1時限目 https://nodejs.connpass.com/event/82614/
JavaScriptユニットテスト⼊入⾨門@sota1235関⻄西Node学園 梅梅⽥田キャンパス1時限⽬目2018/4/20
View Slide
先週の⽇日曜⽇日• 温泉⼊入りながら資料料作ってた• 考えれば考える程「ユニットテストの書き⽅方はプロに任せたほうがいいのでは…?サバンナの⼈人とか」と思い始めた• もっとフロントエンドの⼈人が苦しんでそうなことは無いかと考えた
JavaScriptユニットテスト理理想と現実@sota1235関⻄西Node学園 梅梅⽥田キャンパス1時限⽬目2018/4/20
console.log(me)• Sota Sugiura(きりん)• @sota1235• Mercari, Inc.• 将来の夢はJavaScriptになることです
東京から来ました
今⽇日の話• テストについて
今⽇日の話• (いろんな恩恵を受けるために)テスト(を書きたいんだけど現実問題、古いコードベースとか設計も何もないコードがあってソースがバンドルされてるわけでもない時、私達はどうするべきなのか)について• 現実のつらみからテストを書ける状態に持っていく話をします• テストの書き⽅方はほとんど話しません
アジェンダ• 第1章 テストの必要性• 第2章 現実との戦い• 第3章 モジュールを切り出す
第1章 テストの必要性
なぜテストは必要か• バグ防⽌止?• 負荷確認?• イレギュラー対策?
なぜテストは必要か• バグ防⽌止?• 負荷確認?• イレギュラー対策?• →品質担保のため
品質とは• 意図した通りに動作するか• きれいなコードか• 想定していない⼊入⼒力力に耐性があるか
ユニットテストとは• あるモジュールがある単⼀一の責務を果たしているかをテストする• 最⼩小粒度でのテストを⾏行行う• モジュール単位での品質を担保する
例例えばͤͶ • たこ焼きが美味しいと思った時にクリックする”せやね”ボタン• ボタンを押すとカウントアップして押下済みになる
ͤͶ 1. Clickされる2. APIと通信する3. 通信が返ったらボタンを押下済みCSSにする4. 数字をAPIレスポンスを元に更更新する
ͤͶ 1. Clickされる2. APIと通信する3. 通信が返ったらボタンを押下済みCSSにする4. 数字をAPIレスポンスを元に更更新するそれぞれをテストする
責務ごとにテストを分けると• 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;});});
テスト書きづらい…(´・ω・`)
なぜ書きづらいか• このファイルを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ͤͶ • たこ焼きが美味しいと思った時にクリックするせやねボタン• ボタンを押すとカウントアップして押下済みになる
ユーザインタラクションとロジックを分離する
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ž
ロジックを分離する
ロジックの分離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レスポンスを元に更更新する
ロジックの分離/*** @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
ご清聴ありがとうございました