[Ruby] fizzbuzz 問題を切り分けて考えるとテストが書きやすくなる
いままではこれを書いて満足していた。
[markdown]
“`ruby
(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)
“`
## テストや改変に強い形式に書き直す
なるほど。
> * [RubyでFizzBuzz問題を解いて上司に対抗しよう!](http://melborne.github.io/2011/10/09/Ruby-FizzBuzz/)
>
> 僕はFizzBuzz問題は次のような、3つの小さな問題に切り分けられると思うんだ。
>
> 1. 1つの数を取ってFizzBuzzの結果を返す関数を作る問題
> 2. 1からxまでの数をその関数に適用する関数を作る問題
> 3. スクリプト引数xを2の関数に与えて結果をターミナルに出力する関数を作る問題
>
> 経験あるプログラマはこれらを瞬時に頭の中でやってしまうから、ぼくらの気持ちがわからないんだね。次のようなコードをよく見るけど、個人的には問題の切り分けができてないから、良いコードとは思えないんだよ。
> テストしづらいし改変にも弱いからね。
### 1つの数を取ってFizzBuzzの結果を返す関数を作る問題
まずクラスに書き直してみた。
`return` がないとうまく動かない。
“`ruby:fizzbuzz.rb
#
# 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 でテストを書きます。
> * [library test/unit (Ruby 2.0.0)](https://docs.ruby-lang.org/ja/2.0.0/library/test=2funit.html)
“`ruby:test/test_fizzbuzz.rb
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
``` [文中](http://melborne.github.io/2011/10/09/Ruby-FizzBuzz/)の `mod_zero = ->base{ n%base == 0 }` 部分が理解できないけれどもあきらめて進める(後述)。
> 少し良くなったと思うけど、個人的にはwhenの順位を考慮しなきゃいけないってのが好きじゃないんだ。これはどうかな?
なるほど。文字列を作ってしまうのか。
“`ruby:fizzbuzz.rb
def fizzbuzz(num)
str = ”
str << 'Fizz' if (num % 3).zero?
str << 'Buzz' if (num % 5).zero?
str.empty? ? num : str
end
``` テストを確認すると違う書き方をしていた。
setup に答えを用意して、これを利用する。 ```ruby:test/test_fizzbuzz.rb
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 が[思い浮かばない](https://www.d-wood.com/blog/2014/02/27_5706.html)人生。。。
> * [instance method Enumerable#collect (Ruby 2.3.0)](https://docs.ruby-lang.org/ja/2.3.0/method/Enumerable/i/map.html)
さらにテストの書き方が分からずズルをした。
こちらも setup の答えを利用する。
“`ruby:test/test_fuzzbuzz.rb
def test_map_upto
assert_equal(@ans, @fizz_buzz.map_upto(15, @fizz_buzz.method(:fizzbuzz)))
end
“`
ここまでをまとめるとこうなる。
“`ruby:fizzbuzz.rb
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` でまわしている。 ```ruby:fizzbuzz.rb
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 時には呼ばれないとのこと。
> * [ライブラリ中にテストコードを書く”if __FILE__ == $0″ – ハード屋のヨコ好き](http://d.hatena.ne.jp/shingo-zukunashi/20121010/1349868645)
>
> ライブラリ中にこのように記載した箇所は、直接実行した場合には呼ばれるが、requireした時にはよばれません。
さらにテストコードを見ると `->` が再出。調べると lambda の省略記法だった。
> * [若手エンジニア/初心者のためのRuby 2.1入門(8):Rubyの面白さを理解するためのメソッド、ブロック、Proc、lambda、クロージャの基本 (3/3) – @IT](http://www.atmarkit.co.jp/ait/articles/1409/29/news035_3.html)
> * [class Proc (Ruby 2.3.0)](https://docs.ruby-lang.org/ja/2.3.0/class/Proc.html)
STDOUT をテストするコードが全く分からず。
動かすのにはまって、もっとも時間をとられた。
> * [ruby – Testing STDOUT output in Rspec – Stack Overflow](http://stackoverflow.com/questions/16507067/testing-stdout-output-in-rspec)
> * [Capturing output with UnitTest in Ruby | 42](http://www.42.mach7x.com/2015/11/05/capturing-output-with-unittest-in-ruby/)
スクリプト引数の渡し方もはまった。
下記を読んで setup に `ARGV` のべた書きをしたら動いた。
> * [command line arguments in unit/test – Ruby Forum](https://www.ruby-forum.com/topic/1351796)
“`ruby:test/test_fizzbuzz.rb
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
“`
## まとめ
最終的には、下記のようになった。
> * [DriftwoodJP/training-fizzbuzz at feature/fizzbuzz_02](https://github.com/DriftwoodJP/training-fizzbuzz/tree/feature/fizzbuzz_02)
いろいろと足りない点や気づきが得られた。
STDOUT をテストするコードは、rspec だと簡単にかけるよう。
あとでまとめる。
> * [STDOUT の output を Rspec 3 でテストする | deadwood](https://www.d-wood.com/blog/2016/12/09_8672.html)
## 補遺
> * [FizzBuzz問題を使って社内プログラミングコンテストを開催してみた – give IT a try](http://blog.jnito.com/entry/20111007/1317976730)
> * [どうしてプログラマに・・・プログラムが書けないのか?](http://www.aoky.net/articles/jeff_atwood/why_cant_programmers_program.htm)
[/markdown]