nigoblog

技術系会社のCEOブログ~私的編~

TDD(テスト駆動開発)でハノイの塔の実装をしてみる~TDD超入門~

プログラマーは今こそアルゴリズムを書くべき!!2~再帰アルゴリズムでハノイの塔を解く~ - nigoblog
こんな記事を前回書いて、そこで実装したものも公開しました。
実装方法は単純にアルゴリズムを書いただけなんですが、今回新たに
TDD(Test Driven Development : テスト駆動開発)
で実装する方法を示していきます。

  1. 参考図書
  2. TDDで開発する理由
  3. テストコード
  4. テストコードの実行とハノイの塔の実装

このような流れで説明していきます。

参考図書

たのしい開発 スタートアップRuby

たのしい開発 スタートアップRuby


この本の第7章自動化されたテストを参考にします。この本ではフィボナッチ数列でしたが、今回はハノイの塔で行なっていきます。

TDDで開発する理由

簡単にTDDを説明すると、
テストを書き、その結果をみてどんどん書きなおしていく。
というのを自分の定義とします。
正直いって、個人的にはまだまだTDDの有用性はわからないのですが、使っていくうちにわかっていくのかなと。
今思うのは、テストのエラーを修正していく形で開発することによって、最終的にエラーがないコードが一発で作れることがメリットかなと。
今までは
作る→テスト→直す→テスト→。。。→完成
ですが、TDDでは
テスト→直す→テスト→。。。→完成
と、最初の作るがない。
流れの違いは以上ですが、有用性については今後見つけていきたいと思います。

テストコード

それでは早速テストコードを示します。適当なディレクトリを用意してファイルを作成してください。
hanoi_test.rb

  1 require 'test/unit'
  2 require './hanoi'
  3 
  4 class HanoiTest < Test::Unit::TestCase
  5   def setup
  6     @hanoi = Hanoi.new
  7   end
  8 
  9   def test_disc1
 10     r = @hanoi.disc 1
 11     assert_equal(1, r)
 12   end
 13 
 14   def test_disc4
 15     r = @hanoi.disc 4
 16     assert_equal(15, r)
 17   end
 18 
 19   def teardown
 20   end
 21 
 22 end

1行目はテストコードのライブラリを呼び出しています
2行目はテストの対象となるファイルを呼び出しています
中身ですが、
5行目はハノイの塔のインスタンスを作成します。つまりテスト対象となるクラスのインスタンスの生成。
9行目はテストを実行するメソッドです。rにdiscが1枚の時の値を代入し、それが1であるか確認するメソッドです。
ここでハノイの塔クラスで枚数に対し、移動コストを求めるメソッドをdiscとします。
14行目は9行目と同様です。
19行目でテストの「後始末」をします。setup ~ teardownの間をテストするということですが、あまり詳しく触れる必要はないでしょう。

テストコードの実行とハノイの塔の実装

> ruby hanoi_test.rb

とすると次のような結果が返ってきます

in `require'
	from hanoi_test.rb:2:in `<main>'

要するに2行目のファイルが見当たらないみたいな感じです。
なので同じディレクトリで次のようなファイルを作成します。
hanoi.rb

  1 class Hanoi
  2 end

そしてテストコードを実行すると

Run options: 

# Running tests:

EE

Finished tests in 0.001973s, 1013.6847 tests/s, 0.0000 assertions/s.

  1) Error:
test_disc1(HanoiTest):
NoMethodError: undefined method `disc' for #<Hanoi:0x007fdb4a1940b0>
    hanoi_test.rb:10:in `test_disc1'

  2) Error:
test_disc4(HanoiTest):
NoMethodError: undefined method `disc' for #<Hanoi:0x007fdb4a193610>
    hanoi_test.rb:15:in `test_disc4'

2 tests, 0 assertions, 0 failures, 2 errors, 0 skips

2つのエラーが現れました。ここでエラーにはErrorとFailureの2つがあります。

  • Errorはassertが実行されない場合
  • Failureはassertで期待していた物が違う場合

こんな違いです。
というわけでエラーを読むとdiscっていうメソッドがHanoiクラスにないってことを言っているのでdiscを作ります。
hanoi.rb

  1 class Hanoi
  2   def disc
  3   end
  4 end

ただ作っただけですがテストコードを実行してみます。

Run options: 

# Running tests:

EE

Finished tests in 0.004668s, 428.4490 tests/s, 0.0000 assertions/s.

  1) Error:
test_disc1(HanoiTest):
ArgumentError: wrong number of arguments (1 for 0)
    /Users/nigorinumahiroki/prog/ruby/hanoi.rb:2:in `disc'
    hanoi_test.rb:10:in `test_disc1'

  2) Error:
test_disc4(HanoiTest):
ArgumentError: wrong number of arguments (1 for 0)
    /Users/nigorinumahiroki/prog/ruby/hanoi.rb:2:in `disc'
    hanoi_test.rb:15:in `test_disc4'

2 tests, 0 assertions, 0 failures, 2 errors, 0 skips

こんどはArgumentErrorつまり引数に関わるエラーが発生しました。今回の場合引数が0のメソッドに1つ引数を入れてしまったというようなことが書かれています。
なのでhanoi.rbを書きなおします。
hanoi.rb

  1 class Hanoi
  2   def disc(n)
  3   end
  4 end

ここで実行すると

Run options: 

# Running tests:

FF

Finished tests in 0.004638s, 431.2204 tests/s, 431.2204 assertions/s.

  1) Failure:
test_disc1(HanoiTest) [hanoi_test.rb:11]:
<1> expected but was
<nil>.

  2) Failure:
test_disc4(HanoiTest) [hanoi_test.rb:16]:
<15> expected but was
<nil>.

2 tests, 2 assertions, 2 failures, 0 errors, 0 skips

となりました。今回はアサーションが実行されているということがわかります。ただし、どちらも期待していた値になっていないということが判明しました。なので次のように変更します。
hanoi.rb

  1 class Hanoi
  2   def disc(n)
  3     1
  4   end
  5 end

単純に1を返すというメソッドです。
実行すると

Run options: 

# Running tests:

.F

Finished tests in 0.002629s, 760.7455 tests/s, 760.7455 assertions/s.

  1) Failure:
test_disc4(HanoiTest) [hanoi_test.rb:16]:
<15> expected but was
<1>.

2 tests, 2 assertions, 1 failures, 0 errors, 0 skips

Failureが一つ減りました。つまり, discが1の時に1回の移動というようなことはできていることが言えます。
しかし、discが4の時は満たしていません。というわけで両方を満たすようなコード、つまりハノイの塔のアルゴリズムを実装します。
hanoi.rb

  1 class Hanoi
  2   def disc(n)
  3     if n == 1
  4       1
  5     else
  6       disc(n-1) + 1 + disc(n-1)
  7     end              
  8   end
  9 end

実行すると

Run options: 

# Running tests:

..

Finished tests in 0.001738s, 1150.7480 tests/s, 1150.7480 assertions/s.

2 tests, 2 assertions, 0 failures, 0 errors, 0 skips

FailureもErrorもなし、完璧!!
っていうことでハノイの塔が完成しました。
しかし、ここである疑問があります。
そもそもテストコードが完璧なの?
ということです。
例えば n に0 やマイナスの値を入れた場合は?というテストが実行されていません。
なのでhanoi_test.rbに次のようなコードを挿入します。
hanoi_test.rb

 19   def test_disc0
 20     assert_raise RuntimeError do
 21       @hanoi.disc 0
 22     end     
 23   end

簡単に説明すると、0が入力としてきた時に、RuntimeError(例外エラー)が来て欲しいというようなテストです。
実行すると

Run options: 

# Running tests:

F..

Finished tests in 0.009109s, 329.3446 tests/s, 329.3446 assertions/s.

  1) Failure:
test_disc0(HanoiTest) [hanoi_test.rb:20]:
[RuntimeError] exception expected, not
Class: <SystemStackError>
Message: <"stack level too deep">
---Backtrace---
/Users/nigorinumahiroki/prog/ruby/hanoi.rb:6
---------------

3 tests, 3 assertions, 1 failures, 0 errors, 0 skips

するとFailureとなってしまい、0を入力したときはは例外エラーではないという結果となりました。
なのでhanoi.rbを次のように変更します。
hanoi.rb

  1 class Hanoi
  2   def disc(n)
  3     raise "invalid index" unless n > 0
  4     if n == 1
  5       1
  6     else
  7       disc(n-1) + 1 + disc(n-1)
  8     end
  9   end
 10 end

3行目にn 以上でなければ有効でないというような記述をします。
すると

Run options: 

# Running tests:

...

Finished tests in 0.003866s, 775.9959 tests/s, 775.9959 assertions/s.

3 tests, 3 assertions, 0 failures, 0 errors, 0 skips

というわけで成功しました。

以上TDDの簡単な流れについて説明しました。
ここで書いたことは

たのしい開発 スタートアップRuby

たのしい開発 スタートアップRuby

  • 作者: 大場寧子,大場光一郎,五十嵐邦明,櫻井達生
  • 出版社/メーカー: 技術評論社
  • 発売日: 2012/07/31
  • メディア: 単行本(ソフトカバー)
  • 購入: 2人 クリック: 71回
  • この商品を含むブログ (9件) を見る

(p185~)からかなりインスパイアされております。
詳しく知りたい方はこの本を読むことをおすすめします。

以上TDDについてでした。