はじめに
初めまして。株式会社サイバーエージェント ソフトウェアエンジニアの大澤翔吾と申します。私の所属している部署、メディアディベロップメント事業本部ではAmebaを始めとする自社メディアのマネタイズを行っており、中でも広告によるマネタイズを行うために数多くのアドテクノロジーシステムを開発しています。
タイトルの「Javaとの比較」という文言が目についた方もいるかも知れません。なぜ比較が必要なのでしょうか? それは、比較することによってGoがその特異な思想を実現するために何を犠牲にしたのかを、より明確に知ることができるからです。Goは新興の言語としては異常ともいえるほど簡潔です。「学び始めてから3日で生産的になれる」とすらいわれているほどです。一方でJavaは、ともすれば古い、冗長、生産性が低いといった印象を持たれがちです。こうした印象は部分的には正しいのですが、各言語のコインの片側しか見ていないようなものだと思います。Goの驚異的な簡潔さは、何の犠牲もなしに手に入るものでしょうか? Javaはその歴史の中で何の資産も築いて来なかったのでしょうか? そんなはずはありませんね。コインには必ず表と裏があります。この記事を通じて、Goのコインの裏側を、テスト可能性という観点から知っていただければ幸いです。
Goは、その簡潔さや書く際の生産性ばかりがもてはやされ、メンテナンス性やテストといった観点から語られることが少ないように思います。Goアプリケーションのテストを実際に書いてみて感じたことは、Goは既存の言語と比べるとテスト可能な設計の制約や「やれるけどやってはいけないこと」が多く、自動化単体テストを実践するには方法論への理解と、それを実践し切るという意思が要求されるということです。なぜそう感じたかを説明する前に、単体テストの定義から復習しましょう。
単体テストの定義
自動テストの重要性が叫ばれて久しい今日ですが、自動テストを実施している企業が大多数を占めるというわけではありません。ソフトウェア開発において最先端を行く米国ですら、単体テストの自動化を習慣化できている企業は4割に過ぎないといわれています。ですから、単体テストという最も基本的なテストの定義をしっかり復習し、実施において何が要求されるかを把握しておくことには意味があるでしょう。
単体テスト(unit testing)とは、アプリケーションのユニットに対して行うテストのことです。ユニットとは、アプリケーション内の興味深い振る舞いを持つ小さいコードのかたまりのことで、オブジェクト指向言語ならクラスやメソッドが、その他の言語なら関数やパッケージなどが該当します。
この定義はなんだか自明な響きすらしますが、「ユニットに対して行う」という点に明言されていない重要な条件があります。それは、あるユニットのテストを行うとき、そのユニットが依存する他のユニットのコードは実行してはならないということです。この条件は、単体テスト入門の書籍などでも明言されていることは少ないのですが、例えば、この記事などで採用されています。私も、この条件は絶対に満たさなければならないと実体験を通じて考えるようになったほど重要なものです。
この点をよく理解するために、図1に示す呼び出し階層(call hierarchy)を持ったアプリケーションを例に考えてみます。このアプリケーションは5つのユニットからなり、Mainユニットがアプリケーションのエントリーポイントです。Mainユニットは内部で3つのユニットA、 B、 Cを呼び出し、ユニットBは更に内部でユニットDを呼び出します。このとき、MainはユニットA、 B、 Cに依存している(depend)といいます。同様に、ユニットBはユニットDに依存していますね。
もしMainを単体テストしたいのならば、テスト時、MainはA、 B、 Cの処理を呼び出してはいけません。その代わり、図2に示すように、A、 B、 Cに対応するテスト時専用の実装α、 β、 γを用意し、そちらを使うようにするべきなのです。このように、あるユニットが依存しているユニットを切り替えられるようにすることを依存性の分離(dependency isolation)といいます。また、テスト時専用の実装は頻繁にモック(mock)と呼ばれます。