DT日記

家を離れた自宅警備員の日記

やる夫で学べTDD(1)

TDDとは Test-Driven Development (テスト駆動開発) のことで、つまりはテストを動かしながら開発を進める方式のこと。

なんとなくは聴きかじってたけど、やる夫で学ぶTDDなるものを見つけたので、これに従って書いてみることにする。

ってことで、以下は読みながらのメモ。

第一話前編

インスペクション
ja.Wp - ソフトウェアインスペクション を参照
俗っぽいことばでは人間コンパイラとかgrep が近いか
ソフトウェアテスト技法ドリル
TDD入門の名著、らしい。僕はもちろん、持ってない
なんかamazon.co.jpだと中古で値段が釣り上がってるね。

第一話後編

マイヤーズの三角形
テスト技法の伝統的な問題のひとつらしい
三角形の辺を入力して、不等辺三角形・二等辺三角形・正三角形のいづれであるかを判定する

さしあたって「正三角形か否か」を判定することになる。言語は指定されてないので、ならばとこちらは Ruby で書いてみることにする。

蛇足だけど、一話冒頭で ActiveX ばりばりの C# の話をしてたから本篇でも C# なのかと思ったら、コード例は Java…?

RSpec はちょびっとだけ書いたことがあるのだけれど、Ruby に最初から含まれてる Test::Unit を使ってみる。

やらない夫の誘導により、実装より先にテストケースから

require 'test/unit'
require './triangle.rb'

class TC_Triangle < Test::Unit::TestCase
  def setup
    @triangle = Triangle.new
  end
  def test_equilateral
    @triangle = Triangle.new
    @triangle.a = 1
    @triangle.b = 1
    @triangle.c = 1
    assert @triangle.is_equilteral_triangle?, 'should be passed.'
  end
end

引き続いて、一番単純に書いてみる。

class Triangle
  attr_accessor :a, :b, :c
  def is_equilteral_triangle?
    true
  end
end

ほかの言語と違ってるのは、 attr_accessora, b, c のアクセサを作ってるのと、メソッド名が ? の付いた is_equilteral_triangle? なあたりか。

Ruby では bool 値を返すメソッドは is_hogehoge? みたいな ? 付きの名前にすることが推奨されてる。

で、この状態でテストに (無条件で true を返すので当然) 成功するので、続いてやる夫と同じ実装をしてみる。

class Triangle
  attr_accessor :a, :b, :c
  def is_equilteral_triangle?
    @a == @b && @b == @c && @c == @a
  end
end

うん、これでもグリーン。(いまになって気付いたけど、「グリーン」が何なのか説明されてない…)

しかし、やらない夫から反例が呈示される。

  def test_equilateral
    @triangle.a = 0
    @triangle.b = 0
    @triangle.c = 0
    assert @triangle.is_equilteral_triangle?, 'zero!'
  end

三辺の長さが [0, 0, 0] だと、そもそも図形ですらないから、これでは困る。

そんなときは、えいやっとこんな感じに。

  def is_equilteral_triangle?
    @a > 0 && @b > 0 && @c > 0 &&
      @a == @b && @b == @c && @c == @a
  end

おkおk、これで辺の長さ0が排除できる。ついでに、実在しない負の数の長さも排除できますね。

またまたやらない夫の反例。

  def test_equilateral
    @triangle.a = nil
    @triangle.b = 1
    @triangle.c = 1
    assert !@triangle.is_equilteral_triangle?, 'nil!'
  end

要するに、数字が欲しいのに、それ以外の型が入っちゃ困る、と。

本篇では Java なのでa != null && b != null && c != nullと書くことでぬるぽを回避してるけど、こっちは Ruby なのでちょっと違ったアプローチで書いてみることにする。

  def is_equilteral_triangle?
    return false unless [@a, @b, @c].find{ |n| n.is_a?(Numeric) && (n > 0) }
    @a == @b && @b == @c && @c == @a
  end

Array#findメソッドは、渡したブロックを評価して nil / false 以外になると、それを返す。返さなければ nil が返る。

ブロック内では、即席で作った配列の中身をそれぞれ評価して、値が数字かつ正の数である場合のみパスするようになってる。Ruby における数の値は全て Numeric を継承する。Object#is_a? は引数に渡された定数のクラスもしくは継承したクラスならば true を返す。

この場合のunlessは修飾子で、expr unless condと書くと、condnil/falseのときにexprが実行される。

うん、これで良さげなので、最後にテストケースをまとめてみる。

ユニットテストの流儀とか全然わからないので、こんな書き方で良いのかしら。

require 'test/unit'
require './triangle.rb'

class TC_Triangle < Test::Unit::TestCase
  def setup
    @triangle = Triangle.new
  end

  def test_equilateral
    @triangle.a = 1
    @triangle.b = 1
    @triangle.c = 1
    assert @triangle.is_equilteral_triangle?, 'should be pass...'

    @triangle.a = 2
    @triangle.b = 1
    @triangle.c = 1
    assert !@triangle.is_equilteral_triangle?

    @triangle.a = 0
    @triangle.b = 0
    @triangle.c = 0
    assert !@triangle.is_equilteral_triangle?, 'zero!'

    @triangle.a = nil
    @triangle.b = 0
    @triangle.c = nil
    assert !@triangle.is_equilteral_triangle? , 'nil!'
  end
end

実装コードはこちら

class Triangle
  attr_accessor :a, :b, :c
  def is_equilteral_triangle?
    unless [@a, @b, @c].find{ |n| n.is_a?(Numeric) && (n > 0) }
      false
    else
      @a == @b && @b == @c && @c == @a
    end
  end
end

ぶっちゃけオリジナルのJava版と大して違ってないけど、a.compareTo(BigDecimal.ZERO) == 1 の嵐みたいな文字数の多い判別式ではないのでメソッドは切り出さない。イテレータでまとめたぶん、処理の記述を分けた効果は期待できるかも。

やる夫よろしくスーパープログラマーになった気分なので、次回は第2話の「二等辺三角形」に挑戦したいです。