[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]