・ごあいさつ
はじめまして、ちゃっぴぃです。
はじめまして、ちゃっぴぃです。
シャノンに入社して以来、
SE(って何?) → QAエンジニア → QAマネジャー → 開発エンジニア (←イマココ)
という若干ひねくれた経歴で仕事をしてきました。
QA(Quality Assurance)関連の仕事がこれまで一番長く、得意分野は主にテスト方面です。開発エンジニアとしてはまだまだ新米なのですが、プロダクトを「外から」テストしてきたQA経験を活かして、今はユニットテストなどプロダクトの品質を「内から」高めていこう、としているところです。(ちなみに、テストツールとしては Selenium をそれなりに使ってきたりもしたので、そのあたりのネタもいずれこの場で書くことがあるかもしれません。もしリクエストあらばぜひ。。)
・今日のおはなし
というわけ(?)で、今回は私がお勉強中の書籍『レガシーコード改善ガイド』についてご紹介します。
・『レガシーコード改善ガイド』って何の本?
レガシーなアプリケーション = ユニットテストが整備されていない(ちょっと残念な)アプリケーションのメンテナンスや機能拡張をしなければいけない(これまた残念な)状況で、どのようにコード改善に取り組んでいけばよいのかを指南してくださる、ありがたい本です。
「テストがないコードはレガシーコードだ!」
本のカバーにもあるこのことばのとおり、いくら美しい設計だろうが、超絶技巧を凝らしたコーディングだろうが、テストがないならレガシーだというわけです。メンテナンスするなら、テストがないと話にならん、ということでもあります。
なお、ここでいうテストは、 ≒ ユニットテスト(単体テスト) です。つまり、クラスやメソッドのレベルで実行するもので、(たとえばSeleniumでやるような)GUIレベルの統合テストは対象外です。
本書ではユニットテストを作るための改善手法がたくさん紹介されています。が、それを一つずつ紹介していくとちょっとキリがないので、今回は
- なぜユニットテストが大事なのか
- なぜユニットテストを作るが難しいのか
といったあたりを中心に、書いてみたいと思います。
・何故ユニットテストが重要なのか?
シャノンでは主に Perl で WEBアプリケーションを構築し、いわゆる SaaS として提供していますが、主力製品は初期にかなり急ごしらえしたたこともあり、ユニットテストのカバー率は現状まだまだ低いままです。そのため、テストに関してはややQAに多めにリソースを費やし、(Seleniumによる)自動テストおよび手動テストでカバーしています。
開発とQAが分担してプロダクトを作ってる状況では、以下のような疑念が生じがちです。
「QAがチェックしてくれるし、Selenium でがんばってカバーしてくれてるから、別にそんながんばってユニットテスト書かなくてもいいんじゃないの? 」
なぜ、ユニットテストの充実が必要なのでしょうか。
・QAの自動テスト頼みじゃダメな理由1 = フィードバックが遅すぎる
弊社の現在の開発プロセスでいうと、コードを編集した場合、ざっと以下のような流れになります。
修正 → レビュー(0.5日) → 指摘への対応(0 ~ 2.0日) → コミット → テスト環境更新(0.5 ~ 1.0日) → 自動テスト実行(0.5 ~ 1.0日) → 結果確認(0.5 ~ 1.0日) → バグジラ登録 ...
これだと、開発者がコードを修正してからそれが正しかったのかどうかをフィードバックとして受け取るまで、最短でも 2日、ヘタしたら1週間以上(場合によってはもっと)かかることになります。
これは開発サイクルの中では途方もなく長い時間です。とてもアジャイルなんて言えません。開発とQAの間で細かいバグ情報をやりとりするのは本来的には無駄で、なしで済むならそのほうがよいわけです。プロセス全体でみれば、バグをあとから見つけるよりも、そもそもバグを作りこまないほうが効率的です。
開発者にとっても素早いフィードバックが自身の生産性のために重要であり、その点でユニットテストは開発者のためにこそ重要と言えるでしょう。
・QAの自動テスト頼みじゃダメな理由2 = カバー範囲や粒度があわない
QAでみてるのは統合テストのレベルです。プログラムの細かい引数の組み合わせまで網羅してみるのは無理があります。仮にそのレベルまでみようとすると、一般的にGUIを通じたテストは作成にも実行にも時間がかかるので、テスト量および実行速度などの面から非現実的です。
また、統合テストによってどのファイル、コード、分岐がカバーされてるかのマッピングは取るのが難しく、(少なくとも我々の現状では)分かりません。つまり、開発者が自分のコミットした範囲がQAの自動テストで補足されるとは限りません。
・2つの開発のスタンス
本書では、いかの2つの開発スタンスを比較しています。
- Edit and Pray (編集して祈る)
- Cover and Modify (保護して変更する)
前者はつまりテストなしのままコードを変更することです。いつの日かQAあるいは顧客にボロクソ言われることがないように願いながら、楽観的希望をもってコードをコミットするスタイルです。
これに対して後者が、まずテストでコードを保護し、その後で変更するスタイルであり、本書で強調されている取り組み方です。リファクタリングするにしても、まず最低限テストで保護してから、というのが基本原則になります。
・何故ユニットテストでの保護が難しいのか
そんなにユニットテストがいいものならば、どんどんやればいいじゃないかと思うわけですが、現実は甘くないわけで、テストを考えられないまま作成されたコードは往々にしてテストがしにくい状態になっています。
一番のネックとなるのは「依存関係」です。
単体テストを実行するためには、対象のクラスをインスタンス化して、そのメソッドを実行させて結果を確認する、というのが一般的な流れです。対象のクラスやメソッドが依存するものはテスト用に「偽装オブジェクト(Fake Object)」を用意しますが、この依存するものを偽装するコストが高かったり、不可能だったりします。
たとえば、メソッドの中でデータベースへ直接データ登録していてデータベースがないと動かせないといった状況だと、テストでもデータベース自体を用意しなければいけなくなってしまって、環境準備が面倒だったり実行時間が長くなってしまったりするわけです。
テストで利用しにくいということは、ちょっと違う箇所でそのプログラムを利用しようと思っても前提条件が異なると利用できないということであり、
単体で実行しにくい = テスタビリティが低い = 再利用性が低い
という図式が成り立つと言えます。
・依存関係を取り除くための手法
本書では、いかに依存関係を排除するか、そのためのいろいろな手法がカタログ化されています。(25章)
一部そのタイトルだけ抜粋すると、以下のようなものがあります。
- インタフェース抽出
- 実装の抽出
- サブクラス化とメソッドのオーバーライド
- メソッドオブジェクトの取り出し
- Factory Method の抽出とオーバーライド
- メソッドと変数の引き上げ
- 依存関係の押し出し
- etc.
本書では、「インタフェースの抽出」と、それとある意味で表裏一体の「実装の抽出」という手法が頻繁に出てきます。
インタフェースの抽出:
テスト対象のクラスが依存しているクラスからインタフェースを抽出 → テストでインタフェースを実装した偽装クラスを使えるように =テストでは依存先のオブジェクトそのものを生成しなくて済む
実装の抽出:
テスト対象のクラスが依存しているクラスをインタフェースにして、そのインタフェースを使う実装を抽出 → インタフェースを新たに作るのがはばかられる場合(命名の問題など)や、現在のクラスがそのままインタフェース名として適している場合などに
いずれにしても作り出したい状況は、一枚インタフェースとなるレイヤーをはさむことで、テスト時も同じインタフェースを実装したクラスを偽装することができるようにするものです。このような対応は、オブジェクト指向設計における 「依存関係逆転の原則 (Dependency Inversion Principle)」 と呼ばれるプラクティスに則ったものであり、具象クラスに依存するよりもインタフェースや抽象クラスに依存するほうがメンテナンス性がよいとされるものです。
このような改善を計ることで、テストで保護できる領域を増やしていくことができます。
・とは言っても、普段の仕事の中でそんなレガシーコード保護する時間ないよ!
依存関係を解消するための改善やリファクタリングを行いつつテストで保護された領域を増やすことがよいとはいっても、機能追加や修正を行う場合、既存の部分まで含めて面倒みるほどの時間的余裕はない、というのがよくある現実かと思います。本書では、そんな忙しいあなたのために「スプラウトクラス」や「スプラウトメソッド」といった方法も紹介されています。
既存のクラス/メソッドの中に追加のコードをそのまま書いていくのではなく、新しく実装する部分を既存のコードから「発芽(sprout)」させる、すなわち、別クラスや別メソッドに記述して、せめて新しく作る部分は、テストで保護された状態にしよう、というスタンスです。
あまり細かい追加修正のためにこの手法をとると、いちいち細かいクラスやメソッドが増えたりして、一時的に気持ち悪い状態にあなる場合もあります。しかし本書では、テストで保護された領域を増やすほうが重要であり、むしろそれを継続していくことが設計改善の契機にもなる、と主張しています。
・まとめ的なもの
今回はとりあえず、『レガシーコード改善ガイド』の主題についてざらっと書いてみました。
シャノンで開発しているプロダクトも、最初期はかなりの急ごしらえだったこともあり、この本で言うところの「レガシーコード」がまだまだたくさんあります。脱・レガシーを目指して現在進行形でいろいろ改善中です。今後、僕自身も積極的にプロダクトのユニットテスト改善に取り組んでいこうと思ってるので、具体的な改善方法なども、機会があれば記事として紹介してみたいと思います。
シャノンで開発しているプロダクトも、最初期はかなりの急ごしらえだったこともあり、この本で言うところの「レガシーコード」がまだまだたくさんあります。脱・レガシーを目指して現在進行形でいろいろ改善中です。今後、僕自身も積極的にプロダクトのユニットテスト改善に取り組んでいこうと思ってるので、具体的な改善方法なども、機会があれば記事として紹介してみたいと思います。
(ちなみに私たちは、いわゆる xUnit 系のテストフレームワークを Perl 用に移植したものを使ってテストをつくっていっています。テストフレームワークについてはそれを作った先輩エンジニアがきっと、別途紹介してくれるでしょう)
とにかく少しずつでも、テストで保護された領域を増やしていくことで幸せになれます。本書で紹介されているように適切な手順を踏めば、既存の機能を壊すリスクを最小にしつつ前に進むことができます。レガシーコードと戦わなければならない方は、諦めずに改善活動に取り組んで、テストカバレッジを向上させ、安全にコードをメンテナンスできるようにしましょう!