(2015年までの)odaillyjp blog

イベント参加記録とプログラミング系の雑記

TDDBC長岡に参加してきました

友人などにお願いしてペアプロ+TDD(テスト駆動開発)をしているのですが、テストの書き方やリファクタリングの進め方に不安がありました。TDDの思想も含めて一からしっかり学ぶために、巷で評判のTDDBCに参加しようと思ったのですが、都内で開催されるTDDBCは定員オーバーで参加が難しいです。TDDBCは全国で開催されていますので、思い切って都内のTDDBCのことは考えずに、新潟で開催されたTDDBC長岡1.0に参加してきました。

講義

午前は『プログラマが知るべき97のこと』や『SQLアンチパターン』の翻訳をされた和田卓人さんによる講義です。和田さんはソフトウェア開発に必要なものとして下記の3つを挙げられました。

和田さんは3つをまとめて「ソフトウェア開発の三本柱」と呼ぶようにしています。TDDではソフトウェア開発の三本柱を使いこなす必要がありますので、講義ではこれらの思想を学んでいきました。

バージョン管理ツール

プロダクトコードだけではなく、テストコードのバージョンも管理しておく必要があります。和田さんはソフトウェア開発の三本柱の中ではバージョン管理が一番大事だと話されていました。今はGitやMercurialを利用して、コードのバージョンを管理することが多いですが、今回の講義ではバージョン管理の歴史を辿ってCVSについて言及されていました。バージョン管理ツールが存在しなかった頃は、巻物にワークツリーを手書きしてバージョンを管理していたそうですが、「作りたい機能ごとに開発系ブランチを作っておいて、少しづつ修正して、出来た機能から本番系にマージしていく」という流れについては今も昔も変わらないようです。

スティングフレームワーク

スティングフレームワークはテストケースの書き方、記録や検証の方法、自動でテストが行なえる環境を提供するフレームワークです。テストケースのフォーマットを統一することができ、テスト結果の集計までやってくれます。テストはコードを書きながら何度も繰り返しますので、テストを簡単に実行できるようにするためにこのようなツールが生まれました。テストではなくテスティングと現在進行形の名前が付いている理由は、書いたコードの不安をなくしていくために、開発が進行中のときにテストをすることを大事にしているからだそうです。

自動化

コンピュータはどんなにつまらない単純作業でも文句を言わず、ミスをすることもなく、高速で処理します。なので、単純作業はコンピュータに任せて、人間は人間にしかできないこと(考えること、話すこと)をするべきという考えから、自動化が始まりました。テストも単純作業ですので、テストケースを考えることは人間がして、テストはコンピュータにさせるべきです。

TDD

TDDの書籍などを読むと「Unit Test」という言葉が出てきますが、この「Unit Test」の意味の捉え方は人によって違います。一つ一つの関数に対してのテストと捉える方もいますし、一つ一つ顧客からの仕様に対してのテストと捉える方もいます。そこで、テストは「誰が」「何のために」を行うのか考えてストーリーを作り、テストを下記の3つに分けて定義しました。

  • 【Devloper Testing】 開発者が開発促進のために行うテスト
  • 【Customer Testing】 顧客が進捗管理のために行うテスト
  • 【QA Testing】 品質保証担当者が品質保証のために行うテスト

TDDでは開発者が意図した通りにコードが動いているかをテストする必要がありますので、TDDのテストは「Developer Testing」に相当します。
TDDでは「動かない汚いコード」を「動作するきれいなコード」に変えることを目指していきます。「動かない汚いコード」を「動作するきれいなコード」に変える道筋は2つあります。

  • 動作するように修正してから、綺麗にする(下図の青の道筋)
  • きれいにしてから、動作するように修正する(下図の赤の道筋)

f:id:Shindo_Masaya:20130522222707j:plain:w400
「動くようになったらコーディングは終わり」という考えにより、コードをきれいにしてから、動作するコードに変えるのが良いと言われていた頃があったそうですが、コードをきれいにする作業には終わりがないので、最初にきれいなコードを求めようとすると泥沼に嵌ってしまいます。なので、動作するように修正してから、コードをきれいにする(青の道筋)のが良いそうです。
TDDでは「最初にテストコードを書いて、動作しないことを確認する(RED)」「テストをクリアするプロダクトコードを書いて、動作することを確認する(GREEN)」「テストを落とさないようにリファクタリングする(Refactor)」の3つのフェーズを1サイクルとして、開発が終わるまで繰り返していきます。
f:id:Shindo_Masaya:20130522230319j:plain:w400
このサイクルを素早く回すことが開発を良くするポイントになりますので、1サイクルで実装するコードの量は小さくしておくべきです。TODOリストを作っておいて、TODOリストの中から解決しやすい問題を一つ選択して、一つずつ確実に解決していくのが良さそうです。
TDD初心者から「どんなテストを書けばいいの?」「どこまでテストを書けばいいの?」と質問されることがよくあるそうですが、開発者のためのテストなので、開発者が不安に感じているとこだけ書いていくのが良いそうです。getterやsetterのテストは、テストティングフレームワーク以外の部分で勝手にテストしてくれるので、自分でテストを書く必要はありません。
人間は完璧に物事を考えられる訳ではありませんので、テストを書いても、自分が考えていなかったバグは出てしまいます。しかし、テストを書いておけば、少なくとも自分が考えたバグは永久に防ぐことができますので、テストを書くことは大事なことです。

ペアプロ

午後はペアプロでTDDの実践です。ペアプロの問題はこちらを見てください。@saisa6153さんとペアを組んでコーディングしていきました。

問1 validかどうか調べよう

問1は文字列がバージョン番号として正当かどうかをチェックする .valid? メソッドを作ります。まずはこのメソッドを実装するときに自分たちがテストしたいことを紙に書き出して、その後にテストコードを書いていきました。

  describe ".valid?" do
    it "正当なバージョンを表す文字列を渡したときはTrueを返すこと" do
      Version.valid?("JDK7u40").should be_true
    end

    it "正当なバージョンを表さない文字列を渡したときはFalseを返すこと" do
      Version.valid?("hoge").should be_false
      Version.valid?("fuga").should be_false
      Version.valid?("JDK7u9x").should be_false
    end
  end

引数を何も渡さないときや、文字列以外を渡したときのテストはいらないのか話が出ましたが、引数の有無のチェックはプログラムがやってくるし、Rubyでは引数オブジェクトのクラスはあまり重要ではないので、これらのテストは書きませんでした。プロダクトコードはただの正規表現で良いので、特に悩むこともなく終わりました。(少し特別なところは、正規表現の結果をbool値に変換するために、『Rubyベストプラクティス』に書いてあった二重否定!!を使っているところくらいです。)

def self.valid?(name)
  !!(/^JDK\d*u\d*$/ =~ name)
end

テストコードを整理しておいて、次の実装に入ります。

問2 parseしよう

問2ではバージョン番号を持つオブジェクトを返す .parse メソッドを作ります。メソッドの返り値がバージョン番号を持つVersionクラスのオブジェクトであることをテストしたいので、be_a_kind_of を使ってテストを書こうとしたのですが、TAの@kenchanさんに質問したところ、「Rubyの場合は"どのクラスのオブジェクトであるか"よりも"オブジェクトが使いたいメソッドを持っているか"が大事だから、使いたいメソッドを持っているかでテストを考えた方がいい」と教えていただいたので、respond_to を使ってテストを書くことにしました。(後々にタイムラインを見たら、和田さんがそのときの様子をツイートされていました。)


最終的にテストコードとプロダクトコードはこのようになりました。

  • テストコード
  describe ".parse" do
    it "family_numberとupdate_numberメソッドを実行できるオブジェクトを返すこと" do
      Version.parse("JDK7u40").should respond_to("family_number", "update_number")
    end

    it "返されたオブジェクトに正しいfamily_numberがセットされていること" do
      Version.parse("JDK7u40").family_number.should eq 7
      Version.parse("JDK8u20").family_number.should eq 8
    end

    it "返されたオブジェクトに正しいupdate_numberがセットされていること" do
      Version.parse("JDK7u40").update_number.should eq 40
      Version.parse("JDK8u20").update_number.should eq 20
    end

    it "正規のバージョンを表さない文字列を渡したときは例外を返すこと" do
      proc { Version.parse("JDK7u4x")}.should raise_error
    end
  end
  • プロダクトコード
class Version
  attr_reader :family_number, :update_number
  VERSION_NAME_REGEXP = /^JDK(\d*)u(\d*)$/

  def initialize(family_number, update_number)
    @family_number = family_number.to_i
    @update_number = update_number.to_i
  end

  def self.parse(name)
    return raise unless self.valid?(name)
    family_number, update_number = name.scan(VERSION_NAME_REGEXP).first
    Version.new(family_number, update_number)
  end

  # ...

問3 大小比較しよう

問3では2つのオブジェクトの大小を比較できる #lt, #gt メソッドを作ります。どのバージョンのオブジェクトの組み合わせでテストするかが論点となりましたが、ファミリーナンバーが同じだけどアップデートナンバーが違う組み合わせと、ファミリーナンバーが大きい(または小さい)けどアップデートナンバーだけを見たら小さい(または大きい)組み合わせでテストを書いていきました。

  context "バージョン値を持っているとき" do
    before do
      @f6u60 = Version.parse("JDK6u60")
      @f7u35 = Version.parse("JDK7u35")
      @f7u40 = Version.parse("JDK7u40")
      @f7u40_other = Version.parse("JDK7u40")
      @f7u51 = Version.parse("JDK7u51")
      @f8u05 = Version.parse("JDK8u05")
    end

    describe "#lt" do
      it "自分より大きいアップデートナンバーを持つVersionを渡したときはtrueを返すこと" do
        @f7u40.lt(@f7u51).should be_true
      end

      it "自分より小さいアップデートナンバーを持つVersionを渡したときはfalseを返すこと" do
        @f7u40.lt(@f7u35).should be_false
      end

      it "自分より大きいファミリーナンバーを持つVersionを渡したときはtrueを返すこと" do
        @f7u40.lt(@f8u05).should be_true
      end

      it "自分より小さいファミリーナンバーを持つVersionを渡したときはfalseを返すこと" do
        @f7u40.lt(@f6u60).should be_false
      end

      it "同じバージョンを持つVersionを渡したときはfalseを返すこと" do
        @f7u40.lt(@f7u40_other).should be_false
      end
    end

私たちのペアは #lt を実装したところでタイムアップとなりました。

  def lt(version)
    return self.update_number < version.update_number if self.family_number == version.family_number
    self.family_number < version.family_number
  end

残りの問題

帰宅してから残りの問題を解いて、リファクタリングを行い、githubにpushしました。

感想

TDDを実践してみて、テストの書き方やリファクタリングの進め方にはコツがあるので、とにかく実践を繰り返して学んでいくことが大事なのだと感じました。最後に和田さんも「TDDは才能ではなくスキルなので、本を写経したり、たくさんコードを書いて訓練するのが大事だ」と語られていました。
また、ペアプロでパートナーとあれこれ悩みつつ、相談しながら実装していくのは予想以上に楽しかったです。イベント後には懇親会がありましたが、他の参加者から「なぜTDDに興味を持ったのか」や「プログラミングが好き!」など熱い話が聴けて、懇親会もとても楽しかったです。(いつの間にか、私の席のまわりはRubyユーザーの集まりになっていましたので、一人のJAVAユーザを相手に皆でRubyを宣教していました。)

頂き物

f:id:Shindo_Masaya:20130525010454j:plain:w400
写真はTDDの第一人者であるBob Martinさんが「テストを書くのが辛くなっても、テストを書く意思を持ち続けられるように」という考えにより身につけているグリーンバンドです。「acts_as_professional」と書かれています。テストを書くのが辛くなったら、このグリーンバンドを見て、「プロなのだから、テストを書かないといけない」と自分自身を戒めてるそうです。懇親会で和田さんから頂きました。

参考に

このイベントの講義で使われたスライドはSlideShareで公開されています。
http://www.slideshare.net/t_wada/the-spirit-of-tdd
興味ある方は見てみて、是非 TDDBC に参加してください。