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_accessor
で a, 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
と書くと、cond
がnil
/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
の嵐みたいな文字数の多い判別式ではないのでメソッドは切り出さない。イテレータでまとめたぶん、処理の記述を分けた効果は期待できるかも。