PoohSunny's blog

生きるために食べるのか、食べるために生きるのか。

ひのきの棒を駆使してレガシーコードに立ち向かう #TddAdventJp

このエントリは、TDD Advent Calendar jp: 2012 : ATNDの20日目のエントリです。
昨日は、@mike_neckさんのIPA 平成24年度 システムアーキテクト試験 午後2 問1 解答例 with TDDでした。

今日はTDD初心者がひのきの棒(覚えたてのなけなしの知識)を使ってレガシーコードに立ち向かう話をしようと思います。

f:id:swimming_pooh:20121218214619j:plain

レガシーコードにTDD?

TDDと聞くと、なんとなく新規コードや、テストが既にある程度整っているプロダクトに対して行うものというイメージを持つ方もいらっしゃると思います。
というか私がそうでした。

しかし、もちろんですがTDDはレガシーコードにも有効です。

TDDBCなどに参加していいなーと思って、最低限の知識はキャッチアップしてみた。
これからもっと武器強くしてレベルアップしたいと思ってるんだけど、
仕事ではレガシーコードばっかで、TDDの実践できないよー、
と思っている方向けに、最近仕事でレガシーな部分のメンテにこそこそTDDを導入をしている私の体験談を書きます。

まずはレガシーコードの意味が議論になりそうですが、ここではゆるーく以下のどれかに(あるいは複数)該当したらレガシーコードだと思ってください。

  • メソッドが1000行超えてるんだけど....
  • 「動いているソースはいじっちゃだめだよ」と先輩に言われた
  • クラスの責務がめっちゃ多い
  • 自動実行可能なテストがない(これはレガシーコード改善ガイドの表紙にデカデカと書かれている定義だったりもします。)
  • 機能追加やバグフィックスするのが不安で不安で仕方がない

TDDをやってみると得られるもの

さて、それらのレガシーコードに、TDDを導入してみたらもたらされることは、下記のようになります。

  1. バグフィックスでは、先にテストをFailさせる(手元で不具合を再現させる)。そのあとで修正してテストをGreenにするので、その修正によってちゃんと直った! というのが確認できる。
  2. レガシーコードを再生産しなくなる(なのでソフトウェアがだんだん良くなっていくはず)

あと、TDDというよりテスト一般の話ですが、

  1. テストが増えることで、安心して開発できるようになる
  2. テストが増えることで、デバッグの時間がある程度見積もれるようになる

というのもあります。

良いことだらけですね。ただし、やっぱりレガシーコードはTDDにとって強敵です。
なので、ひのきの棒しか装備していないと一撃で全滅必至です。

なので、ひのきの棒でもラスボスに立ち向かう(せめて一矢報いる)ための方法論を考えてみました。

一番最初に

TDDの基礎を学ぼう

それにはなんといってもまずはTDDBCへの参加がおすすめです。
私も参加してみて、目からウロコなことがやまほどありました。何より楽しかったし。
未熟者ですが、次回は運営とか講師とかのお手伝いがしたいなーと思っていたりします。

ひのきの棒でボスと戦う方法を考えた。

まずはの心がまえ

自分がやっている上で最も大事にするのは、「無理をしない」「できる範囲でやる」ということです。

TDDを始めたばかりでレガシーコードに立ち向かおうとすると、たちまち自分の脳内で雑音が聞こえてきます。

  • TDDでテストなんか書いても時間ばかりかかっちゃうよ。
  • ってか、こんな長いメソッドにテストとか書けないって。
  • 今キャッチアップした知識は最新?今でてるもの全部使いこなせるの?
  • どうせライブラリとかすぐ新しくなってすぐ陳腐化しちゃうんでしょ?
  • テスト書くってもどこから書いていいのかわかんないよ!

というわけでいきなり挫けそうになります。

そこで「無理をしない」です。

確かにいろんな制約の中で、できないことは山ほどあります。不安になることもあるでしょう。
でも、それで全てをあきらめれるのは、あまりにもったいない!
なので、無理せず、できるところから、です。以下も基本的にはこの「無理をしない」というゆるーい概念がベースにあります。

100点を目指してできないからって0点にするよりも、
少しでもTDDが実践されて10点になってる方がいいじゃないですか。

テスト書くのに時間ばっかかかっちゃう

確かに、2000行あるメソッドにいきなり全てのパターンを網羅したテスト書け、とか無茶な話です。
そもそもそんなことできるなら世の中からバグなんて(rya

なので、いきなり無理せず、こんなのから始めてはいかがでしょう。

  • 最初は不具合修正時に、その不具合で直る部分についてのみだけ、テスト書いてみたら?

そうすると、修正点とテストを書く場所がフォーカスできるし、
一度バグった場所はまたバグが起きやすいと思うので、ありがたみを感じられる可能性も高いです。

というわけでバグフィックスから始めましょう。

テストで直すのは1行だけど、それが2000行メソッドの中に....

これは実話をもとにしたフィクションですが、レガシーコードって大抵そういうものですよね。

    public boolean doPerform(HttpServletRequest request, SettingManager CorporateSettingManager) throws Exception {
        
        String actionNo = request.getAttribute("action_no");
        //変数などの定義が100行ほど
        
        if (actionNo == null) {
            return false;
        }
        
        if (actionNo == "1") {
            
        } else if (actionNo == "2"){
            // 以下似たようなコードが500行ほど
        } else if (actionNo == "520"){
            if (useAFunctionFlag && functionCode != null && functionCode.length < 20) { // FIXME 最後の条件はfunctionCode.length < 21 が正しい
                hoge();
            }
        }
        // 以下数百行...

ここまでのコードはあるかわかりませんが、一瞬絶望してしまいます。
ただ、ボスを見ただけでひるんではいけません。どこかに弱点があるはずです。
というわけで、こんなときは、
不具合の最小部分をメソッドでくくれないか、を考えましょう。*1

今回は、ちょっと恣意的にもFIXMEコメントをつけた部分がメソッドでくくり出せそうなので、くくりだしてしまいましょう。
このとき、ツールのリファクタ機能を使えるなら、使ってしまいましょう。テストをない状態でソースをいじるならツールが一番安全です。
Eclipseなら、Ctrl + shift + T > Extract Method でいけます。

    //リファクタツール実行後にできたメソッド
    boolean isAvailableFunctionA(String functionCode, boolean useAFunctionFlag) {
        return useAFunctionFlag && functionCode != null && functionCode.length() < 20;
    }

私だったらこの時点でメソッドをstaticにするぐらいのことはやってしまうかも(何かあればコンパイラが怒ってくれるので)。
あとは、通常どおりTDDでテストメソッドを書けばOKです。

    @Test
    public void useFunctionFlagが立っていてfunctionCodeが20文字の時はTrue() {
        boolean actual = TestClass.isAvailableFunctionA("01234567890123456789", true);
        assertTrue(actual);
    }

虫退治とTDDの話は、Tugu Katagiriさんの12日目のエントリーが詳しく乗ってます。

必ず先にRedにして、プロダクションコードを修正して、Greenにしましょう。
そうじゃないとちゃんとその修正で直ったのかがはっきりせず心配になってしまいますからね。

ついでに、このメソッドの他の部分についてもテストがかけるとなおよしですね!

なお、上記のサンプルメソッドでもそうなんですがもっといいやり方*2・いけてない部分*3は山ほどあると思います。
私自身、「これもっと良いやり方あるんだろうなー」と思うことが多々です。
そういう時は、こう考えてとりあえず手を止めないようにしています。

  • 手元でテストがGreenになっているのだから、後でもっと良いやり方があれば、安心してリファクタするのは可能なはずだ。
  • 自分のスキル的に今できることをやろう。良いテストを書くのは超大事だけど、今はそもそもテストがないのだ。

というわけで、現状を良く出来るのなら少しでも手を動かしましょう。
手を動かすときに気付きがあるわけですし。

その他

あと、私がよく現場で悩んでしまったりするのは下記のような問題です。
現状の解決案も書きます。

  • テスト対象のメソッドの中でSQL使っててDBアクセスが剥がせそうにない。えーん。

 →テスト用のDB*4を作って、DBアクセスさせちゃえばいいじゃん!*5

  • 下手に共有メソッドにくくりだしたりすると、後でゴミになったりしやしないか

 →Greenである限りゴミにはならないので、どんどん増やしましょう。もっとよくするいい案を思いついたらリファクタしましょう。

  • あとですっげー遅くなったりしないかな?

 →テストの数が少ないうちは気になりません。遅くなっても手はあります*6。速度を最初気にするより、まずテストを増やすことを考えましょう。

  • 引数に渡すものが大きすぎて困っちゃう。

 →Mockライブラリを練習するいいチャンスだと思う。*7

というわけで、いろいろ不安になるかもですが、
今よりもコードをより良くしている、と思うことができるのなら、
中途半端でも、不十分でも、TDDでテスト書いてみるのがおすすめです。
それによって、気づきは山ほどありました。

もっと根本的なご意見

思考実験していて、こういう声が聞こえてくることがあります。

「どうせスクラッチで書きなおすんだから、今はテスト書かず、新しいプロジェクトでテスト書けばいいじゃない。」

これ、多分失敗します。
TDDは、テストを先に書くことによって、設計もよりよくする手法です。
だから、せめてスクラッチで開発するときは、まっさらなところにテストがかける状態になっていなければなりません。

既存コードにテストも書かず、その原因も深く追求せず、新しいコードにテストを書くことはできないと思います。
(レガシーコードにTDDすると、ここが書きにくかったからこういうフレームワークにしようとか、テストをベースにした設計アイデアができるようになると思います*8。)

最後に

社内でTDDやテストコード作成などをしていて、だいぶいっしょにテストを書いてくれる人が増えてきました。

  • テスト書いてる途中に新しいバグが見つけられてよかった
  • 既存動作の確認が安心してできる

とかいい影響がではじめました。これがつながっていくと楽しいですね。

さーて、明日のエントリーは?

@fukayatsuさんのTDDを続けるために - fukayatsu.dev()です。
以前TDDBCでご一緒しましたね。テストを軸に出会いが増えるのもまた楽しいものです。
というわけで明日のエントリー楽しみにしています!

*1:前出のレガシーコード改善ガイドで「スプラウトメソッド」と言われているプラクティスです。

*2:たとえば、このメソッドに対するテストは同種のものが多数できそうなので、パラメタライズするとか。

*3:たとえば、スコープをデフォルトスコープにしてるけど、それっていいの?とか((privateメソッドのテストができるライブラリもありますし、reflectionとかで自作も可能です。私は会社では自作したものを使っています。でもそれもめんどければ、スコープ広げてもいいのでは?と考えています。テストがあれば動きは管理できますので。

*4:ってか、最初は普通の開発で使っているDBでいいと思います。初期化でデータをきちっと整形すればOK

*5:これは、InfoTalkという勉強会でt_wadaさんに突撃相談して教えてもらいました。感謝。

*6:横にスケールさせるのである程度解決するみたいです。なのでテストが各々独立していることは最初から気を払ったほうが良いです。

*7:すいません。これだけは根性論で解決してます。

*8:ただ実際に実行したことがないので、まだよくわかんないです。