テストのすヽめ【jest】


どうも。今は亡き経営工学科のOBです。今回も参加させていただきました。
最近、文京区に引っ越したこともありちょこちょこ飯田橋を通ることがあるのですがその際、神楽坂キャンパスが目に入ると懐かしさを抱く、、こともなく普通に素通りしてしまうくらい思い入れはないですw

さてそんな僕ですが、今年の7月に転職を致しました。
転職エントリーを書いているので興味ある人はそちらを見ていただければと思います。
そんなこんな半年が経とうとしているのですが、その中で得た学びを1つ抽出して備忘録として書き綴っていこうかなと思います。

前職では

前職でのプロジェクトの状態

  • 半年後のKPIを達成するためにサービスのプロダクトとして何ができるのか、それをするにはいつまでに何をしておかないといけないのかスケジュール組む
  • 「プロダクト優先(=一旦、動けばいい)」という言葉で全てを片付けてテスト書かない
  • リリース後、改修やらテストやらなんやらPDCAを回しコードをいじりまくる
  • もちろんその時も「プロダクト優先(=一旦、動けばいい)」
  • その結果徐々に改修が難しくなる
  • 最終的にコード全体が読みにくく、少しいじるとあちこちが動かなくなり手をつけれなくなる

というのがプロダクトの各所各所で起こっていました

現状

  • 半年後、1年後、3年後と短期〜中期のKPI達成のためにサービスのプロダクトとして何ができるのか、それをするにはいつまでに何をしておかないといけないのかスケジュール組む
  • そのプロダクトがどこまでの「時間軸」で保守されれば良いのか、リリース後どのくらいの頻度で改修が入りそうなのか判断する
  • テストは必ず書く

という感じです。
転職して当初「エンジニアとしてやらなければならないことは、安心できるプロダクトを世の中に出すこと」という話を結構していただいた記憶があります。その考えが結果として良い方向に進んでいる(気が今はしています)。
あと事業のスピードがかなり早いのでアプリケーションはぶっ壊しても問題が出ないように、設計する時は気をつけています。

実際の感覚

前職とはかなり進め方が違うのですが個人的には今の方が健全な進め方だなと思っています。特にテストコードの部分は大きい気がしています。
ただもちろん何が何でもテストを書かなければならないわけではなく、リリース後の改修がほとんど入らずに急ぎ対応とかならテストコードなしでチャチャっと実装するという判断になる。

ここではテストコードの話を中心に話します。

メリット

  • テストコードにより、プロダクトがちゃんと担保できる
  • リーダブルな設計・コードになりやすい
  • ここの設計こんな感じだと良さげだけどどうなんだろうか、みたいなことを気軽に試せる

逆にデメリットですが、いうと僕も転職して初めてテストを描き始めたので最初はキャッチアップやそもそもどう書くことが良いのかなどの時間がかかったくらいですかね。。

「テスト書くとその分時間かかる」説

よくテスト書くと実質コードを2倍書くので時間も1.5~2倍かかる、と言われています。
個人的な感覚としてはそんなこともないかな〜という感じです。むしろテストを書くと設計などで密結合になっているからここは書き直さないとな〜とかの気付きが多く、思考の迷いは少ない気がしています。またリファクタリングやコードをいじるときテストさえ通っていれば良いのでトータルの実装は早かった感覚がありました。

でもそんなこと言ったって・・

難しいですよね〜w。新規なら良いものの既存のテストなしのプロダクトはどうするんだよという声が聞こえそうなのでそこも少し触れておこうかなと思います。

今、僕が前職に戻るなら

  • テストツール導入
  • 実際にテストが通るように1箇所こじあける
  • 事業部の開発と保守部分を分けて管理し、テスト部分も少しずつ進めていく
  • ただその割合は事業部の開発の方が大きくする
  • それでも事業部の方のタスクが流れ込んでくる場面があるかもしれない。そんな時は一旦リリースする
  • → そのあとの改修は再構築するか、改修にある程度の時間を要するなどの判断軸も常に入った議論になることをあらかじめ共有する

以下みたいな感じで管理してスケジュールを組むかな〜というイメージです。

プロダクトタスク 保守タスク
優先度1 優先度1
優先度2 優先度2
優先度3 -
優先度4 -

事業部:保守 = 7:3とかのタスク量にするとか決めて運用してみるかもしれないです。

基本的にはサービスを前に進めることが中心ですが、その時にリリースまでがゴールではなくそのあとの改修も含めて今どうするのかを判断していけるようにしていければとんでもないことにはならないかなと思っています。こんな確認リストがあると良いかもですね。

  • 確認リスト
    • その機能やプロダクトはいつまでにリリースするのがベストなのか
    • そのリリース日の背景や戦略は何か 
    • リリースしたあとの改修頻度はどのくらいなのか 
    • もしリリースしたあとの改修と次のプロジェクトがぶつかった時どちらを優先するか

テストが通るように1箇所こじあける

前職に戻ったらやってみることで「テストが通るように1箇所こじあける」とありますが、イメージわかないこともあると思うのでコードに落としてみます。

例としてjqueryで都道府県のselectを操作してみます。こんなコードがあったとします。

test.js


export default function createPrefecturesSelect(data) {
  let optionHtml = ''

  const deletedDuplicatePrefectureName = data
    .map(obj => obj.value)
    .filter((currentElement, i, self) => self.indexOf(currentElement) === i);

  for (const name of deletedDuplicatePrefectureName) {
    optionHtml += `<option value=${name}>${name}</option>`;
  } 

  $('#prefecture')
    .html('')
    .append('<option value="">都道府県を選択</option>')
    .append(optionHtml);
}

まずはこんな感じでこのtest.jsがテストを通します。

test.spec.js


import createPrefecturesSelect from '../test';
import $ from 'jquery';

describe('CreatePrefecturesSelect', () => {
  const data = [
    { value: '東京都' },
    { value: '東京都' },
    { value: '大阪府' },
    { value: '大阪府' }
  ]

  let html;
  beforeEach(() => {
    document.body.innerHTML = `<select id="prefecture" name="prefecture"></select>`;
    finishHtml = '<select id="prefecture" name="prefecture"><option value="">都道府県を選択</option><option value="東京都">東京都</option><option value="大阪府">大阪府</option></select>';
  });

  it('initialize', () => {
    createPrefecturesSelect(data);
    expect(document.body.innerHTML).toBe(finishHtml);
   });
 });

このtest.jsですが、「データ操作」「domの作成」の2つの役割があるのでこれを分けていきます。

まずは、データ部分を分けます。

test.js


export function createPrefecturesSelect(data) {
  let optionHtml = ''

  for (const name of uniqPrefectureName(data)) {
    optionHtml += `<option value=${name}>${name}</option>`;
  } 

  $('#prefecture')
    .html('')
    .append('<option value="">都道府県を選択</option>')
    .append(optionHtml);
}

export function uniqPrefectureName(data) {
  return data
    .map(obj => obj.value)
    .filter((element, i, self) => self.indexOf(element) === i);
}

「uniqPrefectureName」のテストを加えていきます。(疲れてきたので略しますw)。

そうすると

  • test.sepc.jsでテストが通る
  • uniqPrefectureName.spec.jsのテスト通る

こうなるとuniqPrefectureNameは担保されます、このあとdomを作成する所を整理してそこもテストします。
最終的に

  • test.sepc.jsでテストが通る
  • uniqPrefectureName.spec.jsのテスト通る
  • prefecturesField.spec.jsのテストを通る

と3つができます。こうなると

  • uniqPrefectureName.spec.jsのテスト通る
  • prefecturesField.spec.jsのテストを通る

で今回やりたいことが完結します。なので最終的にtest.sepc.jsを消す。
そうすると残るのが以下になりファクタリングが進んでいきます。

export default class PrefecturesField {
  constructor(data) {
    this.data = data;
    this.optionHtml = '';
  }

  createDom() {
    this.extractChoice();
    this.build();
  }

  build() {
    $('#prefecture')
      .empty()
      .append('<option value="">都道府県を選択</option>')
      .append(this.optionHtml);
  }

  extractChoice() {
    for (const name of this.data) {
      this.optionHtml += `<option value=${name}>${name}</option>`;
    }
  }
}
export default function uniqPrefectureName(data) {
  return data
    .map(obj => obj.value)
    .filter((element, i, self) => self.indexOf(element) === i);
}

進め方のイメージがついていただければ幸いです。

結論

TDDやっていきましょ!