1.座標の足し算

1.1 準備

何はともあれ、テスト駆動型開発とはどういうものなのか簡単な例でやってみましょう。

まずこの2つのRubyで書かれたプログラムから始めます。

---Point.rb----------------------
class Point
    def initialize(x, y)
        @x = x
        @y = y
    end
    
    def getX()
        return @x
    end
    
    def getY()
        return @y
    end

    def ==(point)
        return (@x == point.getX()) && (@y == point.getY())
    end
end
--------------------------------

---PointTest.rb----------------------
require 'test/unit'

require "Point"

class PointTest < Test::Unit::TestCase
    def testGetXY()
        point = Point.new(3, 4)
        assert_equal(3, point.getX())
        assert_equal(4, point.getY())
    end
end
--------------------------------

Point.rbは座標を表すPointクラス、PointTest.rbはPointのテストクラスの実装です。

Point.rbを見てください。ここではクラスPointの定義、3つのメソッドの定義、2つのインスタンス変数の宣言がされています。

rubyでのクラス定義は

    class クラス名
    end

メソッドの定義は、

    def メソッド(引数列)
    end

メンバー変数は、

    @変数名

です。

initializeはインスタンスの初期化のためによばれるメソッドですので、C++を知っている人はコンストラクタのつもりで見ればいいでしょう。これで、x座標とy座標を与えてPointのインスタンスを作ることができます。

getX(),getY()は、それぞれx座標とy座標を返すメソッド、==は等値性を判断するメソッドです。

次にPointTest.rbを見てみます。

require 'test/unit'
require "Point"

requireはC++の#include、Javaのimportだと思ってください。ここで、Test::Unitというユニットテスト用フレームワークとPoint.rbを読み込みます。

Test::UnitはxUnitと呼ばれるテスティングフレームワークのRuby用の実装です。xUnitはさまざまな言語に移植されていますので、自分が普段使っている言語のxUnitも多分入手できるでしょう。もし適当な実装が見つからなかった場合でも、ある程度のものなら自分で実装することも可能です。Kent Beckのテスト駆動開発の本にはxUnitを作る例も載っています。

次にクラス定義です。

class PointTest < Test::Unit::TestCase
end

"<"の右にはスーパークラスを書きます。Test::Unitを使ったテストではTest::Unit::TestCaseをスーパークラスにします。

次のメソッドがユニットテストメソッドです。ユニットテストをするメソッドはtestで始めるのが決まりです。Test::Unitは、testで始まるメソッドをすべてかき集めて実行してくれます。testGetXY()では、PointのgetX()とgetY()をテストしているつもりです。

    point = Point.new(3, 4)

Pointのインスタンスをx座標3、y座標4で作り、一時変数pointにアサインします。Rubyでは小文字で始めれば一時変数になります。型の宣言は必要ありません。RubyにはGabage Collecterもありますので、deleteを気にする必要はありません。

    assert_equal(3, point.getX())
    assert_equal(4, point.getY())

期待したインスタンスが出来ているかどうかチェックしている行です。x座標が3、y座標が4であることを確かめています。assert_equalの最初の引数が期待する結果、2番目の引数がテストするものです。getX()とgetY()では実装が簡単すぎて、わざわざテストする価値はないのではないかとも思いますが、例として実装しておきました。

ファイルの最後にあるifで始まるブロックでユニットテストを実行しているのですが、その内容について今理解する必要はないでしょう。

とにかく実行してみます。

Green bar
>ruby PointTest.rb

Loaded suite PointTest
Started
.
Finished in 0.0 seconds.

1 tests, 2 assertions, 0 failures, 0 errors

1つのテストメソッドtestGetXYと2つのアサートが実行されテストはすべて問題なく終了しました。

ここで、assert_equal(4, point.getY())の4を5に変えてわざとテストを失敗させて見ます。

Red bar
>ruby PointTest.rb
Loaded suite PointTest
Started
F
Finished in 0.051 seconds.

  1) Failure:
testGetXY(PointTest) [PointTest.rb:30]:
<5> expected but was
<4>.

1 tests, 2 assertions, 1 failures, 0 errors

期待値が5なのに4だったというメッセージが出ています。それでは、もとに戻しておきましょう。またテストが通るようになりました。

ここまでが、テスト駆動型開発の実習のための準備です。それでは、みなさんのPCでPointTestを実行してみてください。問題なく実行できることが確認できたらわざとテストを失敗させてみてください。

1.2 plusの実装

テスト駆動型で座標の足し算を実装してみます。TDDでまず最初におこなうのはテストの作成です。PointTestにtestPlus()を追加します。

    def testPlus()
    end

実行します。

Green bar
>ruby PointTest.rb
Loaded suite PointTest
Started
..
Finished in 0.0 seconds.

2 tests, 2 assertions, 0 failures, 0 errors

追加したtestPlusが実行されているのでtestsが2になっています。もちろんエラーにはなりません。空のテストメソッドを追加しただけでテストを動かしたのは、単なるデモンストレーションではありません。私は実際の開発でも、この段階でテストを動かすことが多いです。テストを動かすことによって、このステップまでは間違えていないという確信を得ることができます。TDDではプログラマの不安を最低限に抑え、常に自信を持ってプログラミングに取り組むことを重要視します。
今回はRubyを使っているのでテストの追加も簡単ですが、C++ではもっと複雑な手続きが必要です。コンパイラがこれでもかと表示するエラーメッセージにうんざりしたくなければ、空のテストメソッドを追加した段階でテストしてみることを薦めます。

それでは実際にテストを書いてみます。座標(3, 4)に座標(2, 1)を加えると座標(5, 5)になるというテストです。

    def testPlus()
        assert_equal(Point.new(5, 5), Point.new(3, 4).plus(Point.new(2, 1)))
    end

テストを実行します。

Red bar
>ruby PointTest.rb
Loaded suite PointTest
Started
.E
Finished in 0.0 seconds.

  1) Error:
testPlus(PointTest):
NoMethodError: undefined method `plus' for #
    PointTest.rb:34:in `testPlus'

2 tests, 2 assertions, 0 failures, 1 errors

plusが定義されていないというエラーになりました。

こうなることはもちろんわかっていました。しかし、あえてテストを実行し、予想したエラーになることを確かめ、自分が道を見失っていないことを確認します。 最初はテストの内容にあまりこだわらない方がいいでしょう。どんなテストでもないよりはましです。まず、テストが完全に習慣になるまでがんばりましょう。そうこうしているうちに、あなたもテスト熱中症(Test Infected)に感染していきます。

つぎに、plusメソッドを定義することにします。今度は、Point.rbを編集します。

    def plus(addend)
        return nil
    end

テストを実行します。

Red bar
>ruby PointTest.rb
Loaded suite PointTest
Started
.E
Finished in 0.0 seconds.

  1) Error:
testPlus(PointTest):
NoMethodError: undefined method `getX' for nil:NilClass
    ./Point.rb:17:in `=='
    PointTest.rb:34:in `testPlus'

2 tests, 3 assertions, 0 failures, 1 errors

plusを定義したので未定義のエラーが消えています。ただし、plusは単にnilを返す実装なのでテストは失敗します。すべておもった通りです。プログラムは完全に私のコントロール化にあります。

さて、この状態をTDDではRedと呼びます。実際、Java用のxUnitであるJUnitのGUIでは赤いバーが表示されます。わかりやすいように、このテキストでも赤いバーを表示しています。 Redは、実装が不十分なために、テストが失敗している状態です。TDDでは確信を持ってこの状態を経由します。この状態を経由することで、自分が確かに何をテストしようとしているのか、つまり、何を実装しようとしているのかがはっきりします。また、今書いたテストコードがその実装ができていないことを証明します。今、失敗するテストが、次のステップでは喜びに代わります。

それでは、すぐにテスト成功の喜びを味わいましょう。

    def plus(addend)
        return self.class.new(5, 5)
    end

テストを実行します。

Green bar
>ruby PointTest.rb
Loaded suite PointTest
Started
..
Finished in 0.01 seconds.

2 tests, 3 assertions, 0 failures, 0 errors

これでtestPlusが成功しました。この状態をTDDではGreenと呼びます。JUnitなら緑のバーが表示される場面です。
私は冗談でこんな実装をした訳ではありません。これはTDDでFake Itと呼ぶ定石です。テストが失敗するという恐ろしい状態を一刻も早く脱するためなら、どんな汚い手段を使ってでも正解を返してテストを成功させます。もっとも、一瞬で正しい実装をする自身があればFake Itを省略して実装してしまってもかまいません。しかし、ちょっと行き詰まってしまったらFake Itに戻り、心の平安を取り戻しましょう。

あまりに進むのが遅くてもうイライラしてきたでしょうか。そういう場合は休憩をとりましょう。プログラミング中は冷静さを失ってはいけません。

冷静さを取り戻したらまず最初に、x座標の計算をしましょう。

    def plus(addend)
        return self.class.new(@x + addend.getX(), 5)
    end

テストを実行すると問題なく成功しています。うまくいくようですのでy座標も同様のコードにします。

Green bar
    def plus(addend)
        return self.class.new(@x + addend.getX(), @y + addend.getY())
    end

テストを実行してうまくいくことを確認します。

Green bar

最後に、さらにリファクタリングの余地がないか見直します。特に改善の余地はないようですのでこれでplusの実装は終わりです。リファクタリングというのはプログラムの外部仕様を保ったまま、Clean code that worksを目指して行う改善活動です。TDDはリファクタリングと切り離して考えられません。小さな単位でできるだけ早くリファクタリングに持ち込み、リファクタリングを一つずつじっくり進めるのがTDDの流儀です。

今までのステップをまとめると次のようになります。

1. Red
    テストを書いて失敗させる
2. Green
    テストを成功させる。Fake Itも許される。
3. Refactor
    テストが成功する状態を保ちながら、リファクタリングを進める。

こう言ってもいいでしょう。

1. Fail It
2. Fake It
3. Make It
4. Refactor It

一歩一歩着実に、プログラミングが間違いなくプログラマの意図するとおりに進んでいることを執拗に確かめながら進んでいくTDDのやり方がわかってもらえたでしょうか。

1.3 minus,product,devideの実装

自分でminus,product,devideを実装してください。minusは座標と座標の引き算、productは座標かける数値、devideは座標わる数値です。これがTDDの演習であることを忘れてはいけません。plusで行ったステップを省略することなく行ない、できれば誰かとペアになって、今何をしようとしているのかを説明しながら実装をおこなってください。

1.4 setup,teardown

現在の状態を確認しておきましょう。

---Point.rb----------------------
class Point
    def initialize(x, y)
        @x = x
        @y = y
    end
    
    def getX()
        return @x
    end
    
    def getY()
        return @y
    end
    
    def ==(point)
        return (@x == point.getX()) && (@y == point.getY())
    end
    
    def plus(addend)
        return self.class().new(@x + addend.getX(), @y + addend.getY())
    end
    
    def minus(minuend)
        return self.class().new(@x - minuend.getX(), @y - minuend.getY())
    end
    
    def product(amount)
        return self.class().new(@x * amount, @y * amount)
    end
    
    def devide(amount)
        return self.class().new(@x / amount, @y / amount)
    end
end

--------------------------------

---PointTest.rb----------------------

#ruby PointTest.rb

require 'test/unit'

require "Point"

class PointTest < Test::Unit::TestCase
    def testGetXY()
        point = Point.new(3, 4)
        assert_equal(3, point.getX())
        assert_equal(4, point.getY())
    end
    
    def testPlus()
        assert_equal(Point.new(5, 5), Point.new(3, 4).plus(Point.new(2, 1)))
    end
    
    def testMinus()
        assert_equal(Point.new(1, 3), Point.new(3, 4).minus(Point.new(2, 1)))
    end
    
    def testPoduct()
        assert_equal(Point.new(6, 8), Point.new(3, 4).product(2))
    end
    
    def testDivide()
        assert_equal(Point.new(1, 2), Point.new(3, 4).devide(2))
    end
end
--------------------------------

テストを実行してみます。

Green bar
>ruby PointTest.rb
Loaded suite PointTest
Started
.....
Finished in 0.01 seconds.

5 tests, 6 assertions, 0 failures, 0 errors

さて、PointTestをみるとself.class.new(3, 4)というのが何度も出てきます。このように複数のテストで同じ設定をおこなう場合が多いので、一箇所にまとめて記述する方法が用意してあります。setup()メソッドを使う方法です。

class PointTest < Test::Unit::TestCase
    def setup()
        @point = Point.new(3, 4)
    end
    
    def testGetXY()
        assert_equal(3, @point.getX())
        assert_equal(4, @point.getY())
    end
    
    def testPlus()
        assert_equal(Point.new(5, 5), @point.plus(Point.new(2, 1)))
    end
    
    def testMinus()
        assert_equal(Point.new(1, 3), @point.minus(Point.new(2, 1)))
    end
    
    def testPoduct()
        assert_equal(Point.new(6, 8), @point.product(2))
    end
    
    def testDivide()
        assert_equal(Point.new(1, 2), @point.devide(2))
    end
end

テストを実行してみます。

Green bar
>ruby PointTest.rb
Loaded suite PointTest
Started
.....
Finished in 0.01 seconds.

5 tests, 6 assertions, 0 failures, 0 errors

ちゃんと動作します。

プリント文を入れて、どういう順番で実行されるのか確かめてみましょう。インスタンスを作ったときに呼ばれるinitialize()メソッドと、テストの後始末をするteardown()メソッドも実装してみました。

class PointTest < Test::Unit::TestCase
    def initialize(method)
        super
        puts id().to_s() + " initialize()"
    end

    def setup()
        #id():オブジェクトの識別子を返す。
        #to_s():文字列表現を返す。
        puts id().to_s() + " setup()"
        @point = Point.new(3, 4)
    end
    
    def teardown()
        puts id().to_s() + " teardown()"
    end
    
    def testGetXY()
        puts "testGetXY()"
        assert_equal(3, @point.getX())
        assert_equal(4, @point.getY())
    end
    
    def testPlus()
        puts "testPlus()"
        assert_equal(Point.new(5, 5), @point.plus(Point.new(2, 1)))
    end
    
    def testMinus()
        puts "testMinus()"
        assert_equal(Point.new(1, 3), @point.minus(Point.new(2, 1)))
    end
    
    def testPoduct()
        puts "testPoduct()"
        assert_equal(Point.new(6, 8), @point.product(2))
    end

    def testDivide()
        puts "testDivide()"
        assert_equal(Point.new(1, 2), @point.devide(2))
    end
end

結果はこうなります。

Green bar
>ruby PointTest.rb
23038592 initialize()
23038580 initialize()
23038520 initialize()
23038460 initialize()
23038400 initialize()
Loaded suite PointTest
Started
23038592 setup()
testDevide()
23038592 teardown()
.23038580 setup()
testGetXY()
23038580 teardown()
.23038520 setup()
testMinus()
23038520 teardown()
.23038460 setup()
testPlus()
23038460 teardown()
.23038400 setup()
testPoduct()
23038400 teardown()
.
Finished in 0.01 seconds.

5 tests, 6 assertions, 0 failures, 0 errors

これで各テストメソッド毎にPointTestのインスタンスが作られ、テストメソッドがsetup()とteardown()に挟まれて実行されていることがわかります。setup()はテストの準備のためのメソッド、teardown()はテストの後始末のためのメソッドです。テスト準備はinitialize()ではなくsetup()で行うことに注意してください。ときどき間違えている人を見かけます。テストメソッドの数だけPointTestのインスタンスを作るのは一見無駄のようですが、こうすることによって各テストメソッドが相互に影響を受けることを避け、各テストを独立に保つ便利な仕組みです。各テストが独立でなければ、一つのテストに焦点を絞って開発を進めていくというスタイルは難しくなってしまいます。


[上][次]