[Ruby] fizzbuzz 問題を切り分けて考えるとテストが書きやすくなる
いままではこれを書いて満足していた。
(1..30).each do |i|
if (i % 15).zero?
puts 'FizzBuzz'
elsif (i % 3).zero?
puts 'Fizz'
elsif (i % 5).zero?
puts 'Buzz'
else
puts i
end
end
バージョンは以下の通り。
% ruby -v
ruby 2.3.3p222 (2016-11-21 revision 56859) [x86_64-darwin16]
% gem list |grep test-unit
test-unit (3.1.5)
Contents
テストや改変に強い形式に書き直す
なるほど。
僕はFizzBuzz問題は次のような、3つの小さな問題に切り分けられると思うんだ。
- 1つの数を取ってFizzBuzzの結果を返す関数を作る問題
- 1からxまでの数をその関数に適用する関数を作る問題
- スクリプト引数xを2の関数に与えて結果をターミナルに出力する関数を作る問題
経験あるプログラマはこれらを瞬時に頭の中でやってしまうから、ぼくらの気持ちがわからないんだね。次のようなコードをよく見るけど、個人的には問題の切り分けができてないから、良いコードとは思えないんだよ。
テストしづらいし改変にも弱いからね。
1つの数を取ってFizzBuzzの結果を返す関数を作る問題
まずクラスに書き直してみた。
return
がないとうまく動かない。
#
# Class FizzBuzz
#
class FizzBuzz
# 1つの数を取ってFizzBuzzの結果を返す関数
def fizzbuzz(num)
return 'FizzBuzz' if (num % 15).zero?
return 'Fizz' if (num % 3).zero?
return 'Buzz' if (num % 5).zero?
num
end
# 1からxまでの数をその関数に適用する関数
# スクリプト引数xを2の関数に与えて結果をターミナルに出力する関数
end
@fizz_buzz = FizzBuzz.new
(1..30).each do |i|
puts @fizz_buzz.fizzbuzz(i)
end
使ったことがない Test::Unit でテストを書きます。
require 'test/unit'
require './fizzbuzz'
class TestFizzBuzz < Test::Unit::TestCase
def setup
@fizz_buzz = FizzBuzz.new
end
def test_fizzbuzz
assert_equal 1, @fizz_buzz.fizzbuzz(1)
assert_equal 'Fizz', @fizz_buzz.fizzbuzz(3)
assert_equal 'Buzz', @fizz_buzz.fizzbuzz(5)
assert_equal 'FizzBuzz', @fizz_buzz.fizzbuzz(15)
end
end
文中の mod_zero = ->base{ n%base == 0 }
部分が理解できないけれどもあきらめて進める(後述)。
少し良くなったと思うけど、個人的にはwhenの順位を考慮しなきゃいけないってのが好きじゃないんだ。これはどうかな?
なるほど。文字列を作ってしまうのか。
def fizzbuzz(num)
str = ''
str << 'Fizz' if (num % 3).zero?
str << 'Buzz' if (num % 5).zero?
str.empty? ? num : str
end
テストを確認すると違う書き方をしていた。
setup に答えを用意して、これを利用する。
class TestFizzBuzz < Test::Unit::TestCase
def setup
@fizz_buzz = FizzBuzz.new
@ans = [1, 2, 'Fizz', 4, 'Buzz', 'Fizz', 7, 8, 'Fizz', 'Buzz', 11, 'Fizz', 13, 14, 'FizzBuzz']
end
def test_fizzbuzz
(1..15).each { |n| assert_equal(@ans[n - 1], @fizz_buzz.fizzbuzz(n)) }
end
1からxまでの数をその関数に適用する関数を作る問題
RubyにはEnumeratorがあるから、これはばかみたいに簡単だよね。関数名をmap_uptoにしよう。
Enumerator で map が思い浮かばない人生。。。
さらにテストの書き方が分からずズルをした。
こちらも setup の答えを利用する。
def test_map_upto
assert_equal(@ans, @fizz_buzz.map_upto(15, @fizz_buzz.method(:fizzbuzz)))
end
ここまでをまとめるとこうなる。
class FizzBuzz
# 1つの数を取ってFizzBuzzの結果を返す関数
def fizzbuzz(num)
str = ''
str << 'Fizz' if (num % 3).zero?
str << 'Buzz' if (num % 5).zero?
str.empty? ? num : str
end
# 1からxまでの数をその関数に適用する関数
def map_upto(max_num, fnc)
(1..max_num).map { |n| fnc[n] }
end
# スクリプト引数xを2の関数に与えて結果をターミナルに出力する関数
end
@fizz_buzz = FizzBuzz.new
puts @fizz_buzz.map_upto(30, @fizz_buzz.method(:fizzbuzz))
スクリプト引数xを2の関数に与えて結果をターミナルに出力する関数を作る問題
ターミナルで $ ruby fizzbuzz.rb 15
こういう事をしたいと言うことだった。
受け取った関数にスクリプト引数を与えて each
でまわしている。
class FizzBuzz
# スクリプト引数xを2の関数に与えて結果をターミナルに出力する関数
def console(fnc)
raise 'need an argument of integer' if ARGV[0].nil?
max_num = ARGV[0].to_i
fnc[max_num].each { |e| puts e }
end
end
# require 時には呼ばれない
if __FILE__ == $PROGRAM_NAME
@fizz_buzz = FizzBuzz.new
result = ->(max) { @fizz_buzz.map_upto(max, @fizz_buzz.method(:fizzbuzz)) }
@fizz_buzz.console(result)
end
if __FILE__ == $0
の部分は、require 時には呼ばれないとのこと。
ライブラリ中にこのように記載した箇所は、直接実行した場合には呼ばれるが、requireした時にはよばれません。
さらにテストコードを見ると ->
が再出。調べると lambda の省略記法だった。
STDOUT をテストするコードが全く分からず。
動かすのにはまって、もっとも時間をとられた。
スクリプト引数の渡し方もはまった。
下記を読んで setup に ARGV
のべた書きをしたら動いた。
class TestFizzBuzz < Test::Unit::TestCase
def setup
ARGV[0] = 15
@fizz_buzz = FizzBuzz.new
@ans = [1, 2, 'Fizz', 4, 'Buzz', 'Fizz', 7, 8, 'Fizz', 'Buzz', 11, 'Fizz', 13, 14, 'FizzBuzz']
end
def test_console
$stdout = op = StringIO.new('', 'w')
result = ->(max) { @fizz_buzz.map_upto(max, @fizz_buzz.method(:fizzbuzz)) }
@fizz_buzz.console(result)
out = str2fizzbuzz_list(op.string)
assert_equal(@ans, out)
ensure
$stdout = STDOUT
end
def str2fizzbuzz_list(str)
str.split.map { |n| n =~ /(Fi|Bu)zz/ ? n : n.to_i }
end
end
まとめ
最終的には、下記のようになった。
いろいろと足りない点や気づきが得られた。
STDOUT をテストするコードは、rspec だと簡単にかけるよう。
あとでまとめる。