TDDでデバッグする

バグを見つけたとき、どうするか。プログラマなら3つの選択肢がある。

  1. 直す
  2. 原因を調べる
  3. 再現テストをする

プログラマなら、直感的に脊髄反射的に1番を選ばないだろうか。
バグを見つけた!
このへんが怪しい!
よし直そう!
やった直った!
というのが典型的なプログラマの動物的本能である。いや、そうであるかどうかは知らないけど、わたしはそうでした。

わたしがどういうデバッグをしていたかというと、当たりをつけて直してみて、直らなくて、アテが外れたなあホントはこっちがおかしいのかなーと、別のところも直してみて、ついでにあっちも直してみて、いろいろ直してるうちになにがどう正しいのかわからなくなって、いや待て落ち着け原因をちゃんと把握するんだっと思ってソースを改めて見直したり、print文を埋め込んで流れを追ってみたり、予想と違う動きをするんだけどそれはそれで正しかったり、考えたり唸ったりまたソースを見直して考えたり祈ったりして、やっと本当の原因が見つかって直して、ちゃんと動作するか確認してたらいつのまにか終電がなくなっててあれれ……とかね。

手順で言うと、上記の1番から3番を順にやっていたことになる。自分の経験を思い起こすに、洗練されて効果的な安定したプロセスとは、まあその、すこし違う。鉄火場のいち風景という方が近い。こうした経験のおかげで、バグを探す能力は向上したと思っている。だが経験そのものは唾棄したいものでもある。

TDDプログラマなら、より安定したデバッグができる。バグ探しの苦行はどうしたって楽なものにはならないが、混乱と試行錯誤の中で気づいたら自分の首を絞めていたというような、余計な苦労はしなくてすむ。

TDDプログラマは順序が違う。最初に3番、再現テストから始める。TDDプログラマなので、もちろん再現テストは「書く」のだ。再現テストは、挙動がおかしい部分について、問題が直るまではレッドになり、直ったらグリーンになるように書く。まずは期待する正しい挙動を明らかにして、これからのデバッグのゴールを明快に設定する。

再現テストは意外と難しい場合がある。システム全体で動かしたときは挙動がおかしいのに、単体レベルでテストするとなにもおかしくない、再現しないという目に遭うことは多い。全体でないと再現しないなら、まずは全体のレベルで再現できるテストを書こう。今はゴールを設定するのが大事なのだ。全体のテストで最終ゴールを押さえたら、適度に分解しながら再現できる場所を探していこう。この作業自体が、原因探しの入り口にもなっている。うまい再現テストが書けたら、もう勝ったも同然ということだってある。

再現テストが書けたら、次にやるのは……テストだ。ただし今度は、2番の「原因を調べる」ためのテストである。仕様に対して実装がどういうふうに間違っているのか、テストで探り当てるのだ。正体のわからない相手に対して、四方八方から探りを入れて概観をつかむというイメージだ。パラメータを1変えたらどうなるか? データの長さや順序を変えたら? nullを渡したら? 呼び出しを入れ替えたらどうだろう? あらたなオブジェクトを作ったり、逆に使い回してみたら? 2回繰り返しても変わらないか? Decoratorパターンなら適用の順序を変えたら? Strategyパターンでテスト用の空っぽのStrategyを渡したらどうなる? こうした試行錯誤で、思わぬ結果が得られれば、問題箇所の特定に役立つ。すべて期待通りに動いたとしても、その部分には問題がなさそうだという見当がつき、これはこれで役に立つ。

試行錯誤の中で書くテストは、テストの読みやすさや名前などは気にしなくていい。思いつきで書いているので適切な名前づけは難しいし、今のところは試行錯誤の数を増やすのが大事だ。コピー&ペーストして連番で名前づけしたり、1メソッドの中でどんどんテストを増やしてもいい。後になって状況が落ち着いてから、必要なテストケースだけを残して、あらためてリファクタリングしよう。

テストを書いているうちに原因のあたりがついてきたら、あとは直すのみだ。直すときにも試行錯誤したくなるだろう。たっぷりテストがカバーしてくれているのだから、安心して試行錯誤していい。リファクタリングしながら直すのもよい。コードがごちゃごちゃしているように感じたら、リファクタリングで整理しよう。整理しているうちに自然とバグが直ってしまうことがあるが、それでも構わない。これまで書いてきたテストがグリーンになれば、安心して「直った!」と宣言していいのだ。

バグが直ってテストも通れば、ひと仕事終わりだ。乾杯!だがその前に、TDDプログラマならば、どうしてもやっておきたいことがあるはずだ。テストが通った、グリーンになった、と来れば、次はもちろんリファクタリングである。バグがあった部分は十分に理解しやすく美しいコードになっているだろうか?そもそもバグが起きえないような設計にできないだろうか?似た問題が他に潜んでいそうな場所はないだろうか?

リファクタリングはテストコードにも及ぶ。既存のテストがあれば、なぜ今回のバグがテストで拾えなかったのか見直すべきだ。仕様の漏れや矛盾を見落としていたのなら、その観点でテストを追加・整理しよう。他にも見落としが見つけられるかもしれない。純粋に実装やロジックの間違いだったなら、今回書いたテストは「バグの再発を防ぐ」テストとして残しておこう。

まとめると、TDDによるデバッグは以下の順序になる。ちゃんとRED-GREEN-REFACTORのサイクルになっていることがわかる。

  1. 再現テストをする (RED)
  2. 原因を調べる (RED)
  3. 直す (GREEN)
  4. リファクタリングする (REFACTOR)