「プロになるためのRuby入門」を読みながら「TDDで改札機プログラムを実装」してみた
- test-unit実践
- まずは要件を文章化
- 要件を満たすシナリオの作成
- ディレクトリ準備
- クラスオブジェクトの作成
- シナリオ1の実装
- テストメソッドに処理の順番を仮置きする
- 実装したいコードの確認
- シナリオ1のタスク細分化
- Task1. Gate.new(:ikebukuro)できる, 2. Gate.new(:sinjyuku)できる
- Task3. Ticket.new(120)できる
- Task4. ikebukuro.enter(ticket)できる
- Task5. ticket.stamp(駅名)で乗車駅を保存
- Task6. ikebukuro.enter(ticket)した後、sinjyuku.exit(ticket) #=> falseになる
- Task7. ticket.stamped_atで乗車駅を参照できるように
- Task8. Gateのcalc_fireで生産できるかテスト(あとでプライベート化するかもしれないがその前にテスト)
- シナリオ2
- 疑問点の検討(自問自答)
- 参考にした本について
- あとがき
test-unit実践
今回は昨日学んだテスト駆動開発(以下、TDD)について、test-unitを使いつつ実装してみます。 内容は「プロを目指す人のためのRuby入門 言語仕様からテスト駆動開発・デバッグ技法まで (Software Design plusシリーズ)」に書いてある改札機のプログラムです。
単にコピペしても勉強にならないと思ったので、なるべく本を読まないようにして作ってみました。(とはいえわからないところ含め、変数名などはだいぶ似てしまっている)
まずは要件を文章化
# 改札機プログラム ## 作りたいもの 改札機のプログラム - チケットをお金で買うことができる - 改札機から入ることができる - 料金が正しければ、降りた駅で改札機から出れる - 料金が間違っていれば、改札機から出れない ## 要件 - 山手線 - 駅 - 池袋 - 新宿 - 渋谷 - 値段 - 120yen: 池袋 〜 新宿 - 120yen: 新宿 〜 渋谷 - 200yen: 池袋 〜 渋谷 ## 動作する仕様 `` ikebukuro = Gate.new(:ikebukuro) sibuya = Gate.new(:sibuya) ticket = Ticket.new(120) ikebukuro.enter(ticket) sibuya.exit(ticket) #=> false ticket = Ticket.new(200) ikebukuro.enter(ticket) sibuya.exit(ticket) #=> true ``
要件を満たすシナリオの作成
シナリオ1
- 120円の切符を購入
- 池袋で入場、新宿で出場
- 出場できる
シナリオ2
- 120円切符を購入
- 池袋で入場、渋谷で出場
- 出場できない
シナリオ3
- 200円の切符を購入
- 池袋で入場、渋谷で出場
- 出場できる
シナリオ4
- 120円の切符を購入
- 新宿で入場、渋谷で出場
- 出場できる
ディレクトリ準備
ruby_ticket_machine - lib - gate.rb - test - gate_test.rb
クラスオブジェクトの作成
Redにする(テストから書き始める)
クラスオブジェクト作成の確認から
# frozen_string_literal: true require "test/unit" require "./lib/gate" class GateTest < Test::Unit::TestCase def test_gate assert Gate.new end end
Greenにする
# frozen_string_literal: true class Gate end
Refactor
今回は特に必要がなさそうなので終了
シナリオ1の実装
テストメソッドに処理の順番を仮置きする
def test_gate # 前準備 # 実行 # 検証 # 後片付け(状況により必要) end
実装したいコードの確認
ikebukuro = Gate.new(:ikebukuro) sinjyuku = Gate.new(:sinjyuku) ticket = Ticket.new(120) ikebukuro.enter(ticket) sinjyuku.exit(ticket) #=> true
シナリオ1のタスク細分化
Gate.new(:ikebukuro)
できるGate.new(:sinjyuku)
できるTicket.new(120)
できるikebukuro.enter(ticket)
できるticket.stamp(駅名)
で乗車駅を保存ikebukuro.enter(ticket)
した後、sinjyuku.exit(ticket) #=> false
になるticket.stamped_at
で乗車駅を参照できるようにGate
のcalc_fire
で生産できるかテスト(あとでプライベート化するかもしれないがその前にテスト)
Task1. Gate.new(:ikebukuro)
できる, 2. Gate.new(:sinjyuku)
できる
過去のコードで対応できているので省略
Task3. Ticket.new(120)
できる
Red
# ./test/ticket_test.rb # frozen_string_literal: true require "test/unit" require "./lib/ticket" class TicketTest < Test::Unit::TestCase def test_new assert Ticket.new(120) end end
Green
# ./lib/ticket.rb # frozen_string_literal: true class Ticket def initialize(fare) @fare = fare end end
Refactor
なし
Task4. ikebukuro.enter(ticket)
できる
Red
# ./test/gate_test.rb # 省略 class GateTest < Test::Unit::TestCase # 中略 def test_enter ikebukuro = Gate.new(:ikebukuro) ticket = Ticket.new(120) ikebukuro.enter(ticket) end # 省略
Green
# ./lib/gate.rb # frozen_string_literal: true class Gate # 中略 def enter(ticket) true end end
Refactor
なし
Task5. ticket.stamp(駅名)
で乗車駅を保存
Red
# ./test/ticket_test.rb # 中略 class TicketTest < Test::Unit::TestCase # 中略 def test_stamp ticket = Ticket.new(120) assert ticket.stamp(:ikebukuro) end
Green
# ./lib/ticket.rb # 中略 class Ticket # 中略 def stamp(station) @boarding_station = station end end
Refactor
なし
Task6. ikebukuro.enter(ticket)
した後、sinjyuku.exit(ticket) #=> false
になる
Red
# ./test/gate_test.rb # 中略 class GateTest < Test::Unit::TestCase # 中略 def test_exit # 前準備 ikebukuro = Gate.new(:ikebukuro) sinjyuku = Gate.new(:sinjyuku) # 実行 ticket = Ticket.new(120) ikebukuro.enter(ticket) # 検証 assert sinjyuku.exit(ticket) # 後片付け(状況により必要) end end
Green
# ./lib/gate.rb # 中略 class Gate # 中略 def exit(ticket) true end end
Refactor
なし
Task7. ticket.stamped_at
で乗車駅を参照できるように
Red
# ./test/ticket_test.rb # 中略 class TicketTest < Test::Unit::TestCase # 中略 def test_stamped_at ticket = Ticket.new(120) ticket.stamp(:ikebukuro) assert_equal(:ikebukuro, ticket.stamped_at) end end
Green
# ./lib/ticket.rb # 中略 class Ticket # 中略 def stamped_at @boarding_station end end
Refactor
なし
Task8. Gate
のcalc_fire
で生産できるかテスト(あとでプライベート化するかもしれないがその前にテスト)
Red
あとでプライベート化するかもしれないがその前にGate.calc_fire
のテスト
# ./test/gate_test.rb # 中略 class GateTest < Test::Unit::TestCase # 中略 def test_calc_fire # 前準備 ikebukuro = Gate.new(:ikebukuro) sinjyuku = Gate.new(:sinjyuku) # 実行 ticket = Ticket.new(120) ikebukuro.enter(ticket) # 検証 assert sinjyuku.calc_fire(:sinjyuku) end end
Green
※追加行は+
、削除行は-
を先頭に付与しています
# frozen_string_literal: true class Gate STATIONS = [:ikebukuro, :sinjyuku, :sibuya] FARES = [120, 200] # 中略 def exit(ticket) - true + calc_fire(ticket.stamped_at) end def calc_fire(exit_station) distance = (STATIONS.index(@station) - STATIONS.index(exit_station)).abs FARES[distance - 1] end end
Refactor
Gate.exit
が成否ではなく、計算結果を返しているようになっているので修正
# ./lib/gate.rb # 中略 class Gate # 中略 def exit(ticket) pay_of(ticket) end private def pay_of(ticket) 0 <= (ticket.fare - calc_fire(ticket.stamped_at)) ? true : false end end
# ./lib/ticket.rb # 中略 class Ticket attr_reader :fare # 中略 end
シナリオ2
# ./test/gate_test.rb # 中略 class GateTest < Test::Unit::TestCase # 中略 def test_exit_scenario_2 # 前準備 ikebukuro = Gate.new(:ikebukuro) sibuya = Gate.new(:sibuya) # 実行 ticket = Ticket.new(120) ikebukuro.enter(ticket) # 検証 assert_equal(false, sibuya.exit(ticket)) # 後片付け(状況により必要) end def test_exit_scenario_3 # 前準備 ikebukuro = Gate.new(:ikebukuro) sibuya = Gate.new(:sibuya) # 実行 ticket = Ticket.new(200) ikebukuro.enter(ticket) # 検証 assert sibuya.exit(ticket) # 後片付け(状況により必要) end def test_exit_scenario_4 # 前準備 sinjyuku = Gate.new(:sinjyuku) sibuya = Gate.new(:sibuya) # 実行 ticket = Ticket.new(120) sinjyuku.enter(ticket) # 検証 assert sibuya.exit(ticket) # 後片付け(状況により必要) end end
疑問点の検討(自問自答)
- 他のクラスをテストする必要があったらどうすればいい?
- 例
- シナリオ1でgateクラスのtestをしている際、tickeクラスの話が登場してきた
- しょうがない時もある
- どういう場合はどっちのクラスに書いたほうがいいなどルールはある?
- シナリオ1でgateクラスのtestをしている際、tickeクラスの話が登場してきた
- 例
参考にした本について
プロを目指す人のためのRuby入門 言語仕様からテスト駆動開発・デバッグ技法まで (Software Design plusシリーズ)
めっちゃオススメです!
Ruby初学者の方にオススメなので、本屋さんなどでぜひ手にとってみてはいかがでしょうか?
著者の方について、ブログなどにもたくさん情報が載っているので貼っておきます。
give IT a try - 伊藤淳一 (id:JunichiIto)
あとがき
やり方が悪かったのか、正直「TDDのサイクルが回っている実感」はありませんでした。
もう少しテストの粒度を大きくした方がいいのか(今回は初めてなのであえて小さくしましたが)、それともRefactorできるところがあったのか。
テスト駆動開発について書かれた本ではないので、そういった本に書かれているプログラムを作ってみたらまた違ったのかもしれません。
明日以降はRailsのテストを書く予定なので、そこでまた検討してみたいと思います。