ありがちなレガシーなプロジェクトにテストを作っていってます。
気がつけばコチコチになっていたプロジェクト
基盤が 10 年以上前に制作されていて、それなりに売上があったために、売上を維持することにしか意識が向いてこなかったプロジェクトがありました。
それから 10 年が過ぎ
- リリースのたびにテスターが悲鳴を上げる
- クリティカルなバグ障害がカジュアルに発生する
- エンジニアは多数いるが、恐ろしく生産性が低い
- 生産性が低いため、新機能がまったくリリースできない
気がつくと、こういった状況にに陥っていました。
しっかりとメンテされていないレガシーな現場
そのプロジェクト特徴をあらわすと、こうです。
- グローバル変数超便利 !
- static 関数多数
- 年月が経っていびつに成長したコード
- コードレビューなにそれ ?
- 自動テストなにそれ ?
全然関係がないと思っていたら、実は課金のできないバグが混入していた、くらいのレベルのことが何度もあります。
するとエンジニアの心理として、 副作用が恐ろしくてコードを触れない 、となってしまいます。
PHP 製なんです。
動的型付けで、制約がゆるいために、適当に型がついてていてもそれなりに動いてしまうんですねぇ。
よくこれ現場の人頑張ってるな、と、感心すらしますね。
とにかくバグが多く、しかも自動テストがないために、修正を施す場合の影響範囲が全くわかりません。
そのための対策の一環として、UnitTest テストを導入する、という話が持ち上がり巻いた。
目視チェックより、テストによる検証をするべき
プログラムは必ず書かれたとおりに動作をしますが、人間だと必ずミスをします。
この特性の違いが重要です。
例えば、経験の浅いプログラマに「関数返り値が期待通りになることを評価してください」というと、画面や標準出力なりに結果を出して、目視チェックをしようとします。
これは大変非効率です。
具体例を出します。
- テストケース A の場合の結果は
AAAAA
になるべき - テストケース B の場合の結果は
BBBBB
になるべき
というテストがあったとして、
AAAAA
や BBBBB
という文字列を目視でチェックするのは、とても大変なことだと思いませんか?
返り値が AAAA
や BBBB
と、 1 文字少なくなっていたとしても、 AAAAA
と AAAA
を見分けるのはツラい、という感覚をまず持ってもらいたいです。
もちろん、 AAAAA
と AAAA
を見分けることはできます。
ただ、それが 1、2回 ならば良いですが、プログラムに手を入れるたびにテストはするべきで、 1,000 回これを見分けることは苦痛です。
また「 AAAAA
になるべき」「 BBBBB
になるべき」という、「期待する結果」を把握して置かなければならない、ということも苦痛です。
コードが大きくなっていって、テストパターンが増えれば、さらにそのコストは増大します。
このように、テストすべき項目はかんたんに増加します。
ボブおじさんの書籍に「テスト項目所の束を見せられて驚愕した」というエピソードがありますが、それを対応する人にとっては笑い話ではありません。
テストを人力でこなすという発想は明らかに非効率です。
非効率なのですが、多くの人が 「画面や標準出力なりに結果を出して、目視でチェックをする」ということの延長で考えているのではないかと思います。
しかしながら、テストパターンとその期待値をすべてプログラムコードとして表現すれば、そして 結果が OK か NG かだけ を出力すれば、繰り返すことのコストを大きく下げることができます。
テスト自動化の意義はここにあります。
テストを構築することで 「うまく走れる」 ようになる
テストは作成することも保守することも多大なコストががかかります。
そのコスト以上にメリットがあるため、これだけテストの必要性が説かれていますよね。
テストを構築することで
- 仕様書がなくても、テストから仕様を把握することが可能となる
- エラーの検知をしやすくする
などの効果が期待できます。
また、うまく構築されたテストはダイナミックな改修をも可能にします。
テストがあることで意図しない副作用を検知できるため、「こわくて回収できない」という硬直状態の解消につながります。
「テストがあることで安心して改修できる」 ようになり、生産性が改善し、うまく走れるようになっていくです。
ただし、ここまでになるにはテストの頭数を揃えて、網羅性を高めるところまで、まずは引っ張り上げる必要があります。
あとからテストを組むことには困難が伴う
テスト自動化には明らかなメリットがあります。
ただし、テストを想定した実装とはなっていないプロジェクトにテストを後入れしていくのは大変難しい場合があります。
手の出しようのないほど大きなクラスや関数は、テストが可能な粒度に分けていく事が必要です。
そうしないと、テスト 1 項目を実行するための事前事後処理が膨大となってしまいます。
また、複雑度が高すぎると、すぐにテストが失敗するナイーブなテストとなってしまいやすく、テストを通すコストが上昇します。
また、具体的には次のようになっていると、まともにテストが動きません。
- 関数の中でセッションやスーパーグローバル変数にアクセスしてる
- static 宣言されている
- exit している
積み上がった負債の代償は大きいです。
そのプロジェクトを、いつまで延命したいか、コストを掛けるだけのリターンがあるか。
レガシープロジェクトの改善は、そういったトータルな費用感も含めた評価が必要ですね。
難しいです。