TDD(テスト駆動開発)でハノイの塔の実装をしてみる~TDD超入門~
プログラマーは今こそアルゴリズムを書くべき!!2~再帰アルゴリズムでハノイの塔を解く~ - nigoblog
こんな記事を前回書いて、そこで実装したものも公開しました。
実装方法は単純にアルゴリズムを書いただけなんですが、今回新たに
TDD(Test Driven Development : テスト駆動開発)
で実装する方法を示していきます。
- 参考図書
- TDDで開発する理由
- テストコード
- テストコードの実行とハノイの塔の実装
このような流れで説明していきます。
参考図書
- 作者: 大場寧子,大場光一郎,五十嵐邦明,櫻井達生
- 出版社/メーカー: 技術評論社
- 発売日: 2012/07/31
- メディア: 単行本(ソフトカバー)
- 購入: 2人 クリック: 71回
- この商品を含むブログ (9件) を見る
この本の第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の簡単な流れについて説明しました。
ここで書いたことは
- 作者: 大場寧子,大場光一郎,五十嵐邦明,櫻井達生
- 出版社/メーカー: 技術評論社
- 発売日: 2012/07/31
- メディア: 単行本(ソフトカバー)
- 購入: 2人 クリック: 71回
- この商品を含むブログ (9件) を見る
(p185~)からかなりインスパイアされております。
詳しく知りたい方はこの本を読むことをおすすめします。
以上TDDについてでした。