なぜリファクタリングは必要なのか?Railsで目指せ、情熱エンジニア(5)

今回からいよいよコードの話を始めます。もはやRubyの文化の一部だという主張もあるリファクタリング、テストについて、その意義や概要を紹介します

» 2011年03月08日 12時00分 公開
[井上真New Bamboo]

実際のアプリを例に解説

 連載のこれまでは、私の過去の体験や仕事環境についてお話ししてきましたがそろそろ少しコードの話をしましょう。

 これからしばらくはテストとリファクタリングがメインです。ただ理論を説明するだけでなく、実際に書かれたアプリのコードをレビューし、テストを追加しながら改善していきます。今回から解説する一連のリファクタリング、テストのターゲットは、@IT編集部の西村賢さんがRuby on Railsで開発中の力作、「Worklista」です。

テストを書かずばRailsエンジニアにあらず?

 2010年11月に米国ニューオリンズで開催されたRubyConfでのキーノートで、Ruby on Railsの作者であるDHHは、テストについて次のように述べていました。

Ruby is about a culture, too. One of these cultures is testing. I found it pretty special when I arrived to Ruby in 2003 that there is such a focus on testing.

 Rubyっていうのは文化でもあるんだ。そしてその文化の中の重要な要素としてテストがある。僕が初めて2003年にRubyにたどり着いたとき、Rubyコミュニティのテストを重視する姿勢がとても特別なものに思えたんだ。

It's not like Ruby invented testing, but there is a culture that really took it in, and made it part of the community core of this is what we do. We test software.

 別にRubyがテストを発明した訳じゃないってことは分かってるけれど、Rubyにはテストがコミュニティの中心部まで根ざしているんだ。「ソフトウェアをテストするのが俺たちのやり方だ」ってね。

There is also another way not to do it. No, no, no. If you are going to be this crowd you have to test your software, otherwise you are not really in it yet.

 テストをしないってやり方もある?チッチッチ、君が僕たちの仲間になりたいのなら君のソフトウェアにテストを書かなきゃ、でないと君はまだここまできてないってことさ。

 ものすごく強い言い方だとは思いますが、確かにRubyコミュニティに、こういう雰囲気があるのも事実です。私が以前参加したRuby on Rails のカンファレンスでも「いやー、今まで一度もテストを書いたことがなくってね」と恥ずかしそうに私に打ち明けてくれた参加者がいました。また会社で新しいプロジェクトを引き受ける際、すべてがすべて新規プロジェクトではなく、既存のプロジェクトを引き継ぐこともあります。そういう時に、まず真っ先にテストのディレクトリを見にいきます。そしてこういったコードを見ると深いため息をつく事があります。

# Replace this with your real tests.
test "the truth" do
  assert true
end

 こういうプロジェクトに限って「レスキュー・ミッション」、つまり、にっちもさっちもいかなくなったプロジェクトを救援するためにかり出されているケースが多々あります。

 とはいってもテストの書き方って、多くの本では最後の章とかに書いてあることが多いですよね。RoRのバイブル的存在である「RailsによるアジャイルWebアプリケーション開発」(原題:Agile Web Development with Rails)は、真ん中ぐらいでテストについてカバーしてありますが、私が読んだときは、もちろんテストの章はすっ飛ばしてアプリを作り始めました。

 始めたばかりのときというのは、新しいコードを書きたくて書きたくてたまらないものですよね。

 それはそれで構わないと思います。いきなり「テストを先に書きなさい」なんて言われると余計無視したくなるものです。ただ、どんどん新しい機能を追加してコードの量が増えてくるにつれ、自分で何を書いているか分からなくなってくるときが来ると思います。そういうときに限って新たなバグなども発見してしまうのですが、「ここを直すと今度はあそこがおかしくなる」と、にっちもさっちも行かなくなってしまいます。そういう経験をされた際に、これから取り上げていく様々な技法を思い出していただければ幸いです。

リファクタリングとは

 「リファクタリング」という言葉が冒頭に出てきました。これは、要はコードを改善することです。もう少し詳しい定義をWikipediaから借りてくるとこうなります。

 リファクタリング (refactoring) とはコンピュータプログラミングにおいて、プログラムの外部から見た動作を変えずにソースコードの内部構造を整理すること。いくつかのリファクタリング手法の総称としても使われる(Wikipedia:リファクタリング

 重要な点は、

  • 外部から見た動作を変えず
  • ソースコードの内部構造を整理する

の2点です。「なんで新しい機能も付けずに今あるコードをいじらなければいけないの?」という疑問を感じた方は以下のもう1つの引用をご覧ください。

 リファクタリングが登場する以前は、一度正常な動作をしたプログラムは二度と手を触れるべきではないと言われていた。下手に手を加えて動作が変わってしまうと、それに伴って関連する部分にも修正が加えられ、やがて修正はプロジェクト全体に波及し対処しきれなくなるかもしれない。またソフトウェアテストを十分に行い正常な動作が確認されたとしても、そのプログラムをわずかでも改変すれば、バグ (欠陥) が見つかったときに改変があったプログラムを疑わなければならない。

 「あのソースコードのあの部分は絶対さわっちゃだめ。理由は良く分からないけど」っていう経験はないでしょうか?

 そして、そのコードの書き方に沿うように他の部分のコードを書いていると、どんどんコードのまとめかたが、それに引きづられて不自然になってしまい、やがては一見外野には簡単そうにみえるコードの修正に何時間も費やすはめになってしまいます。

 リファクタリングによって常日頃からコードが整理整頓されていれば、そのような「ソースコードのブラックホール」をなくすことができるはず、というのがリファクタリングをする動機です。

 でも自分が既存のソースコードを改変したせいで、違うバグが出てくるのは嫌ですよね。そこでテストの出番です。私がここでいうテストというのは、自分でWebサイトをいろいろ挙動を確認するような手動テストではなく、ソースコードをテストするためのコードです。

ソースコードをテストするためのコード

 「ソースコードをテストするためのコード」ってちょっと変な感じがしますが以下が一例です。

require "test/unit"
class TestLibraryFileName < Test::Unit::TestCase
  def test_chop
    assert_equal(-1, chop(3, []))
    assert_equal(-1, chop(3, [1]))
    assert_equal(0,  chop(1, [1]))
    #
    assert_equal(0,  chop(1, [1, 3, 5]))
    assert_equal(1,  chop(3, [1, 3, 5]))
    assert_equal(2,  chop(5, [1, 3, 5]))
    assert_equal(-1, chop(0, [1, 3, 5]))
    assert_equal(-1, chop(2, [1, 3, 5]))
    assert_equal(-1, chop(4, [1, 3, 5]))
    assert_equal(-1, chop(6, [1, 3, 5]))
    #
    assert_equal(0,  chop(1, [1, 3, 5, 7]))
    assert_equal(1,  chop(3, [1, 3, 5, 7]))
    assert_equal(2,  chop(5, [1, 3, 5, 7]))
    assert_equal(3,  chop(7, [1, 3, 5, 7]))
    assert_equal(-1, chop(0, [1, 3, 5, 7]))
    assert_equal(-1, chop(2, [1, 3, 5, 7]))
    assert_equal(-1, chop(4, [1, 3, 5, 7]))
    assert_equal(-1, chop(6, [1, 3, 5, 7]))
    assert_equal(-1, chop(8, [1, 3, 5, 7]))
  end
end

 これは、以前ご紹介したKode Kataのバイナリーチョップの挙動を確認するためのテストコードです。テストには、Rubyに標準で付いて来るTest::Unitを使用してあります。

 assert_equal(-1, chop(3, []))の場合、第一引数の-1が予想値、そして第二引数に実際のメソッドの実行例が載っています。もし実行結果が予想値と同じであれば、このアサーション(断定)は正しいということになります。上記のテストをファイルに保存し、実行すると以下のような結果が出てくるはずです。

Loaded suite untitled
Started
E 
Finished in 0.000263 seconds.
  1) Error:
test_chop(TestLibraryFileName):
ArgumentError: wrong number of arguments (2 for 0)
method chop in untitled document at line 5
method test_chop in untitled document at line 5
1 tests, 0 assertions, 0 failures, 1 errors

 「1 errors」となっていますね。これは実行例のchopメソッドが存在しないことからでたエラーです。

 そこで「def chop(arg1, arg2);end」という1行をファイルの先頭に挿入し、もう一度実行してみましょう。

Loaded suite untitled
Started
F 
Finished in 0.008267 seconds.
  1) Failure:
test_chop:8
expected but was
.
1 tests, 1 assertions, 1 failures, 0 errors

 今度はerrorが消えた代わりに「1 failures」となっています。そこでこの実行結果にマッチするコードを書いていくと、徐々に目的のコードが出来上がっていきます。こうしたコーディング方法は、まずテストを書くことによって作業を進めていくことから、「テスト駆動開発」(TDD:Test Driven Development)と呼ばれています。

 いろんな場面を想定したテストコードをたくさん用意しておき、コードを追加、修正するたびにテストを走らせるようにしておきます。そうしておけば、自分の書いたコードが予期していない動きを見せたときも、すばやくテストがfailしてバグを検知してくれるようになります。

 それでは次回からは、実際のプロジェクトのコードレビューから始め、徐々にテストを追加していきます。専門用語や新しいコンセプトはその都度説明するようにしますね。

Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。