テスト考2014

年々、ウェブアプリを開発するときにテストを書こうという機運が強くなっていると感じる。

これは、開発パラダイムの成熟を意味することであり、基本的に良いことだと思っている。

しかし同時に「テスト原理主義」とでもいうような極端な考え方もでてきていて、開発スタイルをめぐって摩擦が起こっている。

そして、この議論は「テストは、ないよりあったほうが良いよね」という、微視的には誰も反論できないロジックに押し通されがちで、「地獄への道は善意で舗装されている」の典型的な現象に見えて仕方がない。

テストを書かない、というと背景にどんな深い考えがあっても素人くさく聞こえ、逆にテストを書くというだけで良いプログラマーに見える、という非対称な化粧効果がある。ソフトウェア・コンサルティング会社がテスト好きなのは決して偶然ではない。

ソフトウェアというのは、結局のところ、動いてナンボ、使われてナンボである。

期待するものが作れて動きさえすればどんな言語でもいい、どんなに汚いコードでもいい、という考え方がある。ぼくはその考え方には反対だが、その対局にあるのが「テストカバレッジ100%」ならば、もっと強く反対する。

ぼくは会社だけでなく個人でもいくつかのウェブサービスを作ってきているけれど、新しめのプロジェクトではテストはほとんど書かなくなった。それどころか、コメントすらほとんど書かなくなった。そして、それと反比例するように、コード自体のクオリティは、ぐんぐん向上している。

なかなか面白い現象だと思う。

思うに、

  • それなりにできるプログラマが一人で作っている場合には、テストやコメントはほとんど必要ない
  • しかしチームで開発する場合には、他人にコードを壊される機会が増えるので自然とテストを書くようになる
  • 一人でゼロからリリースまでやりぬいた経験のあるプログラマーは1%もおらず、あとの99%はチーム開発しか経験したことがない
  • だからテストは絶対的なものとして君臨する

おおよそ、以上のような流れではないだろうか。

ポール・グレアムは、「頭の中にプログラムを入れる」という有名なエッセイで、「コードの同じ部分を複数の人にいじらせない」といった。「複数の人でプロジェクトをやろうと思うなら、それをコンポーネントに分けて、それぞれを一人が受け持つようにすることだ。」と。しかし、オープンソースやPull Requestベースのワークフローというのは、これの真逆をいくもので、同じコードを不特定多数でワッショイワッショイ編集しまくるので、誰にとっても全体像が見えにくくなっていく。しかし組織としては、ある重要なコンポーネントを一人の人間に依存するのは怖いので、それなりのコストを払ってでもリスクヘッジしたい。まさしく「組織というものを定義付ける資質の一つは、個々人を交換可能なパーツとして扱うということだ。」と上記のエッセイに書かれている通りのことだ。

つまり、テストやコメントは、「個々人を交換可能なパーツとして扱う」ための必要悪なのであって、保険みたいなものだ。

テストはリファクタリングを助ける?邪魔する?

良いコードを維持するためには内部構造をしょっちゅうリファクタリングする必要がある。テストの存在は、外部インターフェースを壊すことなく内部をリファクタリングすることを可能にしてくれるが、一番よく変化し、壊れやすいのは内部構造のほうであったりする。こちらは、残念ながらテストで解決するのには向かない領域だ。(とはいえ、そんなことさえ知らず内部構造に対するテストを書いて自分で自分の足を撃ってるプロジェクトが多いことを考えると、もっと事態は深刻かもしれない)

もし、あなたのアプリが公開されたAPIをもち、それを維持していく必要があるなら、もっとも特徴的なAPI(たとえば認証まわり)いくつかに対してテストを書くべきだ。しかし、すべてのAPIに対してテストを書いてカバレッジ100%を目指すのはやめたほうがよい。おそらく、追加で書かれるテストの存在価値は限りなくゼロに近いし、そうでなければ、そのことのほうが問題だ。

その追加で書かれるテストは、おそらくほとんどが他のAPIからのコピペになるだろう。コピペでコードを書くことは害悪とされているのに、テストならばコピペは許されるのだろうか。

一番よいコードは、何も書かないことである。というと禅問答のようだが、自分で車輪の再発明をするのではなく枯れたライブラリを使うというのも、この範疇である。これは、同じことを実現するコードなら短いほうがよい、と一般化できる。そうした場合に、テストコードというのは実現したいこと(=アプリケーションコードの価値)には影響しないが、書いた行数は増える、すなわちメンテナンスという意味ではアプリケーションコードと同等のコストを払うことになるので、同じことを実現するためのコード量が増え、冗長になったことと等価である。

だから、テストは必ず書くという考え方はばかげている。アプリケーションコードと全く同じで、コストとリターンを見極めながら必要に応じて書く、が正しい。

コメントが必要なコードは抽象度が甘い

これは、経験からいうと、コードのクオリティが上がったからコメントが必要なくなった、というほうが正しいかもしれない。とくにRubyのような自然言語に近い記述ができるパワフルな言語では、コメントに書くぐらいならコードにしてしまったほうが読みやすくなるケースも多い。自然言語で仕様を記述するのにくらべてコードが複雑でコメントが必要になるケースというのは、得てして対象の理解が曖昧で、したがってコードの抽象度が甘いだけのことが多い。

これは、実例を出さないとわかりにくいだろうけど、サクッと出せるシンプルな例題が手元にないのでちょっともどかしい。

それでも、コメントを書く機会はテストよりは多い。なぜならば、未来の自分は別人なので、過去の自分がどういう経緯で今のような実装にしたのか、書いておいてもらわないと理解できないからだ。そういう場合には、悩んで現在のような実装に妥協した理由を、簡潔にコメントとして残しておく。逆に、ダラダラと長くコメントを書く必要があるような甘い実装は許容しないというルールでもある。これは経験上、けっこう良い規律としてワークする。どうしても複雑な事情でワークアラウンドせざるをえない場合には、該当するGithub IssueやStack Overflowなどへのリンクをコメントに貼っておく。なければ自分で外部サイトに書く。とにかくソースコード内にはダラダラ書かない。

エラー通知システムの存在

最後になったけど、時代の進歩という意味で、これが一番大きいかもしれない。

ウェブアプリならAirbrakeHoneybadgerのような例外通知機構を導入することで、ユーザが踏んだバグをもれなくリアルタイムに知ることができるようになった。これらのサービスは、同じ種類のバグレポートが何度も通知されないようになっており、影響範囲の大きいバグをデプロイしてしまったときにもメールボックスが溢れてしまうというようなことがない。

個人的にAirbrakeを使い始めたのは2008年頃だったが、以来、ユーザが目にした500 Internal Server Errorは、もれなく追跡できている。無料なので、あらゆるプロジェクトで使っている。

おかげで、たいていのバグは、デプロイ直後に見つかり、すぐ修正できる。テストなんかより、よっぽど日々の役に立っているという実感がある。

いまAirbrakeの履歴をざっとみて、「これ、テストがあれば防げたなぁ」と感じるバグの割合を探ってみると、5-6%程度もない。つまり、たとえテストのカバレッジ100%であっても防げなかったバグがほとんどだったということになる。ほとんどの場合は、そもそも想定外の手順でリクエストがきたり、壊れたデータがきたりという例外なので、想定した範囲のケースしか扱えないテストで検出できないのは当たり前である。

逆に、こうしてプロダクションにリリースしてから発覚した「想定外の手順」や「想定外のデータ」をレグレッション対策としてテストケースに追加していくのは、ものすごく有効だ。自分の頭で想定できないパターンなので、将来の役に立つ。つまり、予測にもとづいてテストケースを増やすのではなく、実際に起きた問題にもとづいてテストケースを増やしていく。これはYAGNI原則という意味で重要なアプローチの違いである。

その結果、最近では「エラーゼロ」が個人的な目標になっている。新しく開発を続ける限りバグはゼロにはならないけど、とにかくすぐになおす。慢性的に発生する問題をひとつも放置しない。

このゴールを追求すると、プログラミングのレイヤーだけではなく、ミドルウェアやOSやインフラに関する深い知識が必要になるけど、そういったものを学ぶ良いきっかけでもある。

ソフトウェアがある程度の規模になると、エラーゼロというのは現実的ではないと思っていたけれど、そんなのは言い訳にすぎないと思うようになった。

モダンなエラー通知システムを使ったことがないのにソフトウェア品質を語り、テストの有用性を説くというのは、老害以外の何ものでもない。使ったことのない人は2014年のやることリストに「Airbrakeの導入」を追加しておこう。

まとめ

まとめると、今でもテストを書いてるのは以下のようなケースである。

  • 複雑な挙動をするクラスのユニットテスト、とくに境界条件まわり
  • 複雑な正規表現を書くときはTDD的で、テスト環境で例を追加しながら作業する
  • デプロイ後にプロダクションで発生した想定外の外部入力
  • 一度発生したレグレッションの再発予防(先回りはしない)
  • 複雑なステップを踏む特異的なフォーム(サインアップなど)
  • 外部に公開するAPIのうち特徴的なもの(認証や課金など)

こう書くと、すごく大量にあるように思えるかもしれないが、実際のボリュームは極めて少ない。おかげでテストのS/N比は高く、実際に役に立つテストになっている。また、テストには習慣的な側面もあるので、書くのがおっくうにならない程度の周期で書くようにしている。

ウェブアプリ開発が1980年代のミドルウェア開発やゲーム開発などと同じ道のりをたどっていくとするならば、この次にくるのは専業のQAチームが開発チームと独立して存在してリリースのサインオフ権限を握り、想定外のパターンをテストしまくってリリース前にバグを出し切る、という体制になるのかもしれない。

このアプローチが素晴らしいのは、開発者が自分で想定したテストを書いて満足するというマスターベーション的なテストではなく、本当の意味で想定外のケースがテストできることだ。しかし、想像に難くないことだが、開発やリリースのサイクルは劇的に落ちる。しかし、成熟というのにはそういう面もあるのだろう。

まぁ、なんにせよ、現在のウェブアプリ開発におけるテストなんて一歩間違えれば「ままごと」みたいなレベルだから、そんなに原理主義的になるのはダサいよねって話です。

でも、テストを一行も書かないあなたはクビですので念のため。