見出し画像

開発歴20年エンジニアが語る、ユニットテストを書いてリファクタリングをしよう!

皆さまこんにちは。
私は40台も半ばを過ぎたおっさんエンジニアです。
開発歴は気づけば20年を超えてしまいました。

そうした経験の中から今回は私が特に大事だと感じている、リファクタリング・ユニットテストについてお伝えします。

【ライターの紹介】
MASAO
開発事業部所属。設計から運用、フロントエンドからバックエンドまで浅く広く活躍中。基本的にはゲームが好きなインドア人間だが、ランニングにもはまってフルマラソンの自己ベスト2時間46分。

技術的負債について


皆さんは「技術的負債」ってご存知でしょうか。

ソフトウェア開発における概念であり、時間がかかるより良いアプローチを使用する代わりに、今すぐ簡単な(限定的な)解決策を選択することで生じる追加の手直しの暗黙のコストを反映したものである。

上記はWikipediaから技術的負債の説明の一部を抜粋したものです。
システム開発において理想は最初から完璧なコードを書くことですが現実はそうはいきません。
スケジュールに追われて「ひとまずこれでも動くからリリースしよう」
こんなことがけっこうあるものです。例えば以下のコード。

// startからendまでの合計値を返す
public int sum(int start, int end) {
  int ret = 0;
  for(int i=start; i<=end; i++) {
    ret += i;
  }
  return ret;
}

これでも問題なく動くのでいいのですがStreamを使った方が可読性が上がるので望ましいです。
こうしたことも技術的負債といえます。
そしてそのことに後から気づいて書き直すことになったとします。
これをリファクタリングといい技術的負債を返済するための手段の一つとなります。

リファクタリングについて


コンピュータプログラミングにおいて、プログラムの外部から見た動作を変えずにソースコードの内部構造を整理することである。
上記はWikipediaからリファクタリングの説明の一部を抜粋したものです。

さて、先ほどのコードをリファクタリングしましょう。
こんな感じになります。

// startからendまでの合計値を返す
public int sum(int start, int end) {
  return IntStream.rangeClosed(start, end).sum();
}

リファクタリング前のコードは合計値を算出するための手続きが書かれていますが、リファクタリング後のコードは合計値を算出したい旨をそのまま書くのみなのでこちらの方が可読性が高いといえます。

これでよりよいコードになった!と喜んで終わりではありません。
本当にリファクタリング前後で動作が変わってないといえるのか。
この問題を解決する必要があります。

ユニットテストについて


本当にリファクタリング前後で動作が変わってないといえるのか。
この問題を解決する手段がユニットテストを書くことです。

今回の例のコードのユニットテストを書くならこうなります。

@Test
public void testSum() {
  assertThat(example.sum(1, 1), is(1));
  assertThat(example.sum(1, 10), is(55));
  assertThat(example.sum(1, 100), is(5050));
  assertThat(example.sum(101, 100), is(0));
}

処理の動作を担保するのに必要十分なテストケースを用意します。
そうすることでテスト自体がその処理の仕様を示すものになりますし、テストがOKならばリファクタリング前後で動作が変わってないといえるわけです。

ユニットテストが書きやすい設計


ユニットテストの重要性をお伝えできたかと思いますが、ユニットテストが書きにくいケースもあります。

// 引数に乱数を加えて返す
public int addRandomNumber(int num) {
  return num + new Random().nextInt();
}

呼び出す度に結果が変わるのでテストが書けません。
この場合は以下のように処理中でRandomをnewするのではなく、引数でRandomを受けるようにします。

// 引数に乱数を加えて返す
public int addRandomNumber(int num, Random rand) {
  return num + rand.nextInt();
}

これによりRandomのモックを使うことでユニットテストが書けるようになります。

@Test
public void testAddRandonNumber() {
  Random mockRandom = mock(Random.class);
  when(mockRandom.nextInt(1)).thenReturn(7);
  when(mockRandom.nextInt(2)).thenReturn(8);
  when(mockRandom.nextInt(3)).thenReturn(9);
  assertThat(example.addRandonNumber(1), is(8));
  assertThat(example.addRandonNumber(2), is(10));
  assertThat(example.addRandonNumber(3), is(12));
}

このようにとある入力に対して必ず同じ出力となるよう意識するのが大事です。
こうした処理を各々ユニットテストで動作担保していく。
これが技術的負債を返済しやすく高品質を維持しやすいシステム開発につながると考えます。

ALHについて知る



↓ ↓ ↓ 採用サイトはこちら ↓ ↓ ↓


↓ ↓ ↓ コーポレートサイトはこちら ↓ ↓ ↓


↓ ↓ ↓ もっとALHについて知りたい? ↓ ↓ ↓