JavaScriptユニットテストの理想と現実

 JavaScriptユニットテストの理想と現実

Talk at 関西Node学園 梅田キャンパス 1時限目
https://nodejs.connpass.com/event/82614/

046baac588d91fd78a85b189847a151d?s=128

Sota Sugiura

April 20, 2018
Tweet

Transcript

  1. JavaScriptユニットテスト ⼊入⾨門 @sota1235 関⻄西Node学園 梅梅⽥田キャンパス1時限⽬目 2018/4/20

  2. 先週の⽇日曜⽇日 • 温泉⼊入りながら資料料作ってた • 考えれば考える程「ユニットテストの書き⽅方 はプロに任せたほうがいいのでは…?サバン ナの⼈人とか」と思い始めた • もっとフロントエンドの⼈人が苦しんでそうな ことは無いかと考えた

  3. JavaScriptユニットテスト 理理想と現実 @sota1235 関⻄西Node学園 梅梅⽥田キャンパス1時限⽬目 2018/4/20

  4. console.log(me) • Sota Sugiura(きりん) • @sota1235 • Mercari, Inc. •

    将来の夢はJavaScriptに なることです
  5. 東京から来ました

  6. 今⽇日の話 • テストについて

  7. None
  8. 今⽇日の話 • (いろんな恩恵を受けるために)テスト(を書きたい んだけど現実問題、古いコードベースとか設計も 何もないコードがあってソースがバンドルされて るわけでもない時、私達はどうするべきなのか)に ついて • 現実のつらみからテストを書ける状態に持ってい く話をします

    • テストの書き⽅方はほとんど話しません
  9. アジェンダ • 第1章 テストの必要性 • 第2章 現実との戦い • 第3章 モジュールを切り出す

  10. 第1章 テストの必要性

  11. なぜテストは必要か • バグ防⽌止? • 負荷確認? • イレギュラー対策?

  12. なぜテストは必要か • バグ防⽌止? • 負荷確認? • イレギュラー対策? • →品質担保のため

  13. 品質とは • 意図した通りに動作するか • きれいなコードか • 想定していない⼊入⼒力力に耐性があるか

  14. ユニットテストとは • あるモジュールがある単⼀一の責務を果たして いるかをテストする • 最⼩小粒度でのテストを⾏行行う • モジュール単位での品質を担保する

  15. 例例えば ͤ΍Ͷ  • たこ焼きが美味しいと 思った時にクリックす る”せやね”ボタン • ボタンを押すとカウン トアップして押下済み

    になる
  16. ͤ΍Ͷ  1. Clickされる 2. APIと通信する 3. 通信が返ったらボタンを押下 済みCSSにする 4.

    数字をAPIレスポンスを元に 更更新する
  17. ͤ΍Ͷ  1. Clickされる 2. APIと通信する 3. 通信が返ったらボタンを押下 済みCSSにする 4.

    数字をAPIレスポンスを元に 更更新する それぞれをテストする
  18. 責務ごとにテストを分けると • 1テストケースがシンプルになる • モジュールが責務ごとに分かれるよう強制さ れる

  19. 責務ごとにテストを分けると • 1テストケースがシンプルになる • モジュールが責務ごとに分かれるよう強制さ れる

  20. なぜか

  21. 汚いコードはテストが書きづらい

  22. 汚いコードはテストが書きづらい =テストを書けるようにすると綺麗になりやすい

  23. せやねボタン 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; }); });
  24. せやねボタン 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; }); });
  25. テスト書きづらい…(´・ω・`)

  26. なぜ書きづらいか • このファイルをrequireしたら即実⾏行行される • イベントリスナに渡される1つのfunctionがいろ んなことしてる • 分岐の数の掛け算だけテストケースが増える • 不不確定要素が多い

    • APIとの通信、DOM APIのコール
  27. これが現実だ!!!! • ⼊入社して全てのフロントエンドのコードがキ レイって早々ないと思ってる • たぶん

  28. 第2章 現実との戦い

  29. ターゲット • テストが書くのが難しく、またテスト⽂文化も そこまで浸透していない(主観)フロントエンド に着⽬目します

  30. なぜテストが浸透していないか • フロントエンドのコードをモジュールでばら して書けるようになったのがここ数年年 • フロントエンドの要件⾃自体が複雑化した • 複雑化したロジックを保守する必要性が増し た 考察

  31. フロントエンド is カオス • ステートフルな世界 • UIとロジックの2つの世界 • 様々な外部要因 •

    APIとの通信、ローカルストレージ
  32. 現実 is カオス • N年年物のレガシーコード • Webpack?なにそれ美味しいの? • リポジトリに威⾵風堂々と居座るjQuery1.x.0

  33. どう⽴立ち向かうのか • 既存のロジックにユニットテストを書く • 前にコストパフォーマンスを考える

  34. テストは書いて終わりではない • 運⽤用、保守する必要がある • ロジックが変わればテストも書き換える • 品質を担保するためのものに品質を上げる時 間を取られてはいけない

  35. コスパを考える • テスト対象のモジュールは変更更される可能性 が⾼高くないか • テスト対象はロジックでなくUIにまつわるも のでないか

  36. 例例えば • クリックされたら消費税を計算するロジック

  37. 例例えば • クリックされたら消費税を計算するロジック • 計算ロジックだけならテストしてよさそう

  38. 例例えば • クリックされたら消費税を計算するロジック • 計算ロジックだけならテストしてよさそう • jQueryで動的に⽣生成されるDOMのclass名

  39. 例例えば • クリックされたら消費税を計算するロジック • 計算ロジックだけならテストしてよさそう • jQueryで動的に⽣生成されるDOMのclass名 • いろんな都合でclass名変わる可能性が⾼高いしそも そもDOM構造も変わりやすい

  40. テストを書く場所の勘所 • UIのテストは無駄になることが多い • E2Eテストが難しいと⾔言われる所以 • コアのロジックは変わることが少ない • 使い回せる粒度に保てば再利利⽤用性も上がる •

    「社内npmライブラリとして使い回せるか」
  41. 第3章 モジュールを切り出す

  42. モジュールを切り出す • 現実はだいたい多くの責務を持った「何か」 がそこにいる • この章ではその何かを実際にばらしあとはユ ニットテストを書くだけ、という状態にもっ ていく

  43. せやねボタンreturns ͤ΍Ͷ  • たこ焼きが美味しいと 思った時にクリックす るせやねボタン • ボタンを押すとカウン トアップして押下済み

    になる
  44. せやねボタン 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; }); });
  45. ͤ΍Ͷ  1. Clickされる 2. APIと通信する 3. 通信が返ったらボタンを押下 済みCSSにする 4.

    数字をAPIレスポンスを元に 更更新する
  46. ユーザインタラクションとロジックを 分離する

  47. 1. Clickされる 2. APIと通信する 3. 通信が返ったらボタンを押下 済みCSSにする 4. 数字をAPIレスポンスを元に 更更新する

    Ǽ✣nj ƺȉǕǿDžǍǽȉ ȃǎǙDž
  48. 分離する 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);
  49. 分離する 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ž
  50. ロジックを分離する

  51. 1. Clickされる 2. APIと通信する 3. 通信が返ったらボタンを押下 済みCSSにする 4. 数字をAPIレスポンスを元に 更更新する

    Ǽ✣nj ƺȉǕǿDžǍǽȉ ȃǎǙDž
  52. ロジックの分離 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レスポンス を元に更更新する
  53. ロジックの分離 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レスポンス を元に更更新する
  54. ロジックの分離 /** * @param {Response} */ function changeSeyaneButtonStatus(res) { if

    (res.ok) { document .getElementById(‘.seyane-button') .style.color = 'gray'; } } 1. Clickされる 2. APIと通信する 3. 通信が返ったらボタン を押下済みCSSにする 4. 数字をAPIレスポンス を元に更更新する
  55. ロジックの分離 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レスポンス を元に更更新する
  56. ロジックの分離 /** * @param {Number} */ function updateSeyaneButtonCount(count) { document

    .getElementById('.seyane-counter') .innerHTML = count; } 1. Clickされる 2. APIと通信する 3. 通信が返ったらボタン を押下済みCSSにする 4. 数字をAPIレスポンス を元に更更新する
  57. ロジックの分離 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レスポンス を元に更更新する
  58. テスト書けそう

  59. 現実は泥泥臭臭い • 1つのイベントリスナに詰まっていたものをま ずイベントリスナとそれ以外で分けた • その後、ロジックから2つのロジックを切り出 した • 後はファイルを分けてテストを書くだけ!

  60. つらい現実に⽴立ち向かうには • 今あるコードを紐解いていく • 紐解く時の鍵はUIとロジックの境⽬目 • 紐解いてfunction化、module化したものにテ ストを追加していく • 簡単なところから少しずつ、ばらしていく

  61. 余談: テストを書くという⽂文化 • もし今いるチームにテストを書く⽂文化が無い なら積極的にこのアプローチを試してほしい • 1つ簡単なサンプルがあるとみんな真似できる • 新しく追加するロジックでテストを書けるよ うになる

  62. まとめ

  63. まとめ • 現実はつらい • つらくてもテストには価値がある • テストを書くための審美眼を極める • UI, ロジックの境⽬目

    • コスパの良さ
  64. おまけ • 過去に社内向けにmochaのトレーニングリポ ジトリを作りました • 基礎的なテストを書けるようになりたい⽅方は どうぞ • TwitterなりIssueなりで質問待ってます

  65. おまけ • この現実のつらさ を越えた先にある つらさについて過 去に発表したので よければどうぞ https://speakerdeck.com/sota1235/importwomotukusuruhua

  66. ご清聴ありがとうございました