僕がユニットテストをする理由

もし君が、この 15 年間に少しでもソフトウェア開発に携わったことがあるなら、ユニットテストの重要性を誰かがくどくど言うのを、きっと聞いたことがあるはずだ。

マネージャーからこんなセリフを聞いたことはないだろうか。

「ユニットテストって最高じゃないか!コードを文書化してくれるし、バグの混入リスクも減る。変更にかかるコストとリスクも減らせるから作業スピードを落とさないで済む。良いユニットテストは、開発からデプロイまで全体のスピードを上げてくれるよ!」なんて。

確かにこれらはユニットテストをするにあたって、妥当な理由であるけれど、それはあくまでマネジメント上の理由だ。

僕もそういった理由には賛成だ。けれど今述べた理由は、開発者として僕がユニットテストをする理由の本心とは違う。

僕がユニットテストをする理由はとてもシンプルだ。

ユニットテストは、新旧のデザインの向上や、ソフトウェア・デザイナーとしての腕を上げるための絶好のチャンスであり、強いモチベーションの元になる。

コツは、出来るだけ書くユニットテストの数を少なくし、それぞれのテストがシンプルになるようにすること。

これが何故うまく行くか?理由は、シンプルなユニットテストを書くことは、本質的に退屈で、コードが酷ければ酷いほど、テストするのもより難しく退屈になるからだ。

ユニットテストを動かす唯一の方法は、必要なユニットテストが殆どないくらいに劇的に実装を改善して、それからテストを書くことだ。

実装を改善すれば、ユニットテストは減らせる

実際に作成するユニットテストを削減するには、以下のようなアプローチがあげられる。

  • 重複したコードのリファクタリング  もしコードブロックを抽象化できたら、その分ユニットテストの数が減る。
  • デッドコードの削除   削除できるコードにわざわざユニットテストを書く必要はない。こんなこと当たり前だと思ったら、レガシー・コード・ベースをもっと見てみて欲しい。その理由がわかるはずだ。
  • フレームワークのボイラープレートを、 Configuration か Annotation として外部化  そうすれば、 Scaffolding (スキャフォールディング)としてではなく、テストが必要なプロダクトにだけテストを書くことが出来る。
  • コードの各分岐は、最低 1 件のユニットテストが必要になる。だから if 文やループを削除できると、その分テストの件数も減らせるのだ。 実装言語によっては、派生型ポリモーフィズム、コードの移動、プラグイン可能な Strategy パターン、アスペクト指向、 Decorator パターン、高次のコンビネータ等のテクニックを取り入れることで if 文やループを削除できる。コード内に分岐点がある分だけ、追加のテストに依存してしまいかねない。出来るだけ減らそう。
  • もっと奥底にあるデータフローのパターンを見つけて、抽象化する 偶発的な計算処理を取り出すことで、似ていないように見えるコードも、似せることが出来る。一度これを行ってしまえば、基本構造を統合できる。そうすれば、簡単にテスト可能で分岐のないコードをどんどん増やせる。最終的には、シンプルなセマンティック・ルーチン(述語や シンプルなデータ変換が多い)と 10 個くらいの再利用可能なコントロールパターンの連なりが残るだろう。
  • 出来るだけ ビジネス上のロジック、永続性、プロセス間の通信を別々にすることでモックオブジェクトをいじくりまわすのを回避できる。 モックオブジェクトは「コードの臭い」なので、多用している場合、コードが結合しすぎているサインかもしれない。
  • ロジックを一般化する方法を見つける。そうすれば、エッジケースにもメインフローが適用され、多様で複雑なインプットもひとつのテストでカバーできる。特別なケースに対しては、つい他に使い回せないコードを書いてしまいがちだ。しかし、その代わりに特別な取扱いのいらない、もっと一般的なソリューションを探すことだって可能なはず。ただ、注意して欲しいことは、よりシンプルかつ一般的なソリューションを探す方が、特別なケースを沢山作成するよりも、ずっと難しい、ということ。シンプルなコードを少しだけ書くための時間が十分になく、その代わりに複雑なコードを大量に書かなければならない、といったこともあるかもしれない。
  • 既存ライブラリ内のメソッドとして既に実装されているロジックを認識して置き換える。そうすれば、ライブラリの作者にユニットテストを押しつけてしまうことが出来る。
  • もし、データオブジェクトをイミュータブルになるくらい単純化できて、オペレーションも簡単な代数的な規則に従うだけ、という所まで行くと、プロパティに基づいたテストを活用できる。これは文字通り、ユニットテストが自動で書き込みをしてくれる、というもの。

能書きはこれくらいにして、実際にコードを見てみよう。

深いパターンを見つけだし、重複したコードを抽象化する

データサイエンスのコードでよくあるパターンとして、ある関数が最適化されたコレクションの要素を探す、というものがある。これに対する最もシンプルな  Java コードは、以下の様なものになるかと思う。

  double bestValue = Double.MIN_VALUE;
  Job bestJob = null;
  for (Job job : jobs) {
    if (score(job) > bestValue) {
      bestJob = job;
    }
  }
  return bestJob;

このコードは、とても簡単で、考えなくても素早く書けてしまうかもしれない。ループと  if  文だけ!うまく行かないはずないって?

確かに、初めの数回は良いけれど、こういうコードを書く度に「技術的負債」が増えてしまう。ユニットテストを書くと、重複したコードとリスクが段々と見えてくる。コードの各ブロックがテストを必要とするのは、一般的なケースでの正しさを調べるためだけでなく、沢山のエッジケースを調べるためでもある。

でも、もし空のコレクションを渡してしまったら?単一要素のコレクションは? null は?

上のコードみたいにシンプルなコードでも、ユニットテストに引っかかるようなバグを持っていたりする。すると、最適化をしようとする度に、沢山テストを書かなくてはいけなくなる。

あくまで僕の意見だけれど、僕ならもっと別のことに自分の時間を使いたいね。

もっと良いソリューションとしては、こんなものがある。

まず、大事なことに気づいて欲しい。

それは、小さな量の重複コードでも、抽象化、コード作成、テスト実行までの作業は一度きりに出来るし、一度きりにすべきである、ということだ。

これは、コードを一般化してエッジケースの修正をするチャンスにもなる。

    public static <J> J argMax(Iterable<J> collection,
                               Function<J, Double> score) {
      double bestValue = Double.MIN_VALUE;
      J bestElement = null;
      if (collection != null) {
        for (J element : collection) {
          if (score.apply(element) > bestValue) {
            bestElement = element;
          }
        }
      }
      return bestElement;
    }

このコードはユニットテストを一度しか必要としない。

さらに改善するにはこのロジック全てを、ライブラリコールと置き換えればいい。

(以下の例はGoogle の Guava ライブラリ)

  public static <J> J argMax(Iterable<J> collection,
                             Function<J, Double> score) {
    return Ordering.natural().onResultOf(score).max(collection);
  }

その後は、使用するスコアリング関数ごとにユニットテストが必要になるだけ。

他は全て処理済み、となるのである。

ユニットテストを避ければ最高のソフトウェアデザインが見えてくる

この記事に書かれたユニットテスト回避のテクニックは全て、障害に強く柔軟なデザインに欠かすことの出来ないものだ。

これは、ユニットテストをやる・やらないに関わらず当てはまることだ。

しかし僕らはつい、物事を前に進めようと焦るあまり、これを忘れてしまいがちになる。けれど、ユニットテストを継続することで、本当に必要なユニットテストをする時間と理由が出来てくるはずだ。

そして、「ユニットテストをあまり実装しない」という、良い意味での手抜きをすることで、プロジェクトデザインと実装の継続した改善を行えるだろう。

少なくとも、やるかやらないかは君次第。

もし君が、コードの基本デザインを改善せずに、ユニットテストに当てるべき時間をテスト作成に当てているなら、そこから学ぶことは恐らく何もないだろう。

もしかしたら、そもそも「なんとなく動く」以上の質のコードを書く理由がないのかもしれない。

もし君が、プロダクトコードを改良することで、ユニットテストに当てるべき時間そのものが減るように努めているなら、「上質なデザインを持つソフトウェア」が一体どんなものか、きっとすぐに解るはずだ。

あくまで僕の意見だけれど、これだから僕はプログラミングが好きなんだ。

(了)

デイブ・グリフィスは Indeed のソフトウェアエンジニアで、ソフトウェアシステムの開発に 20 年以上携わっています。