Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

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

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

Sota Sugiura

April 20, 2018
Tweet

More Decks by Sota Sugiura

Other Decks in Technology

Transcript

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

    View Slide

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

    View Slide

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

    View Slide

  4. console.log(me)
    • Sota Sugiura(きりん)
    • @sota1235
    • Mercari, Inc.
    • 将来の夢はJavaScriptに
    なることです

    View Slide

  5. 東京から来ました

    View Slide

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

    View Slide

  7. View Slide

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

    View Slide

  9. アジェンダ
    • 第1章 テストの必要性
    • 第2章 現実との戦い
    • 第3章 モジュールを切り出す

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  20. なぜか

    View Slide

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

    View Slide

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

    View Slide

  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;
    });
    });

    View Slide

  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;
    });
    });

    View Slide

  25. テスト書きづらい…(´・ω・`)

    View Slide

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

    View Slide

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

    View Slide

  28. 第2章 現実との戦い

    View Slide

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

    View Slide

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

    考察

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  41. 第3章 モジュールを切り出す

    View Slide

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

    View Slide

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

    View Slide

  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;
    });
    });

    View Slide

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

    View Slide

  46. ユーザインタラクションとロジックを
    分離する

    View Slide

  47. 1. Clickされる
    2. APIと通信する
    3. 通信が返ったらボタンを押下
    済みCSSにする
    4. 数字をAPIレスポンスを元に
    更更新する
    Ǽ✣nj
    ƺȉǕǿDžǍǽȉ
    ȃǎǙDž

    View Slide

  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);

    View Slide

  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ž

    View Slide

  50. ロジックを分離する

    View Slide

  51. 1. Clickされる
    2. APIと通信する
    3. 通信が返ったらボタンを押下
    済みCSSにする
    4. 数字をAPIレスポンスを元に
    更更新する
    Ǽ✣nj
    ƺȉǕǿDžǍǽȉ
    ȃǎǙDž

    View Slide

  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レスポンス
    を元に更更新する

    View Slide

  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レスポンス
    を元に更更新する

    View Slide

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

    View Slide

  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レスポンス
    を元に更更新する

    View Slide

  56. ロジックの分離
    /**
    * @param {Number}
    */
    function updateSeyaneButtonCount(count) {
    document
    .getElementById('.seyane-counter')
    .innerHTML = count;
    }
    1. Clickされる
    2. APIと通信する
    3. 通信が返ったらボタン
    を押下済みCSSにする
    4. 数字をAPIレスポンス
    を元に更更新する

    View Slide

  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レスポンス
    を元に更更新する

    View Slide

  58. テスト書けそう

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  62. まとめ

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide