テスト駆動開発から始めるRuby入門 ~2時間でTDDとリファクタリングのエッセンスを体験する~

初めに

この記事は一応、Ruby 入門者向けの記事ですが同時にテスト駆動開発入門者向けともなっています。

対象レベルによって以下のように読み進められれば効率が良いかと思います。

  • Ruby 入門者でプログラミング初心者・・・とりあえずコードの部分だけを写経しましょう。解説文は最初のうちは何言ってるかわからないと思うので 5 回ぐらい写経して Ruby を書く感覚がつかめてきてから読み直すといいでしょう。もっと知りたくなったら参考図書にあたってください。と言っても結構お高いので「リーダブルコード」と「かんたん Ruby(プログラミングの教科書)」といった初心者向け言語入門書から買い揃えるのがおすすめです。

  • Ruby 経験者でテスト駆動開発初心者・・・コード部分を写経しながら解説文を読み進めていきましょう。短いステップでテスト駆動のリズムが感覚がイメージしていただければ幸いです。もっと知りたくなったら原著の「テスト駆動開発」にあたってくださいオリジナルは Java ですが Ruby で実装してみると多くの学びがあると思います。あと、「プロを目指す人のための Ruby 入門」が対象読者に当たると思います。

  • 他の言語経験者でテスト駆動開発初心者・・・コード部分を自分が使っている言語に置き換えながら解説文を読み進めていきましょう。もっと知りたくなったら原著の「テスト駆動開発」にあたってくださいオリジナルは Java と Python が使われています。あと、「リファクタリング」は初版が Java で第2版が JavaScript で解説されています。

  • 言語もテスト駆動開発もつよつよな人・・・レビューお待ちしております(笑)。オブジェクト指向に関する言及が無いというツッコミですが追加仕様編でそのあたりの解説をする予定です。あと、「リファクタリング」には Ruby エディションもあるのですが日本語訳が絶版となっているので参考からは外しています。

写経するのに環境構築ができない・面倒なひとは こちら からお手軽に始めることができます。

TODO リストから始めるテスト駆動開発

TODO リスト

プログラムを作成するにあたってまず何をすればよいだろうか?私は、まず仕様の確認をして TODO リスト を作るところから始めます。

TODO リスト

何をテストすべきだろうか—-着手する前に、必要になりそうなテストをリストに書き出しておこう。

— テスト駆動開発

仕様

1 から 100 までの数をプリントするプログラムを書け。
ただし 3 の倍数のときは数の代わりに「Fizz」と、5 の倍数のときは「Buzz」とプリントし、
3 と 5 両方の倍数の場合には「FizzBuzz」とプリントすること。

仕様の内容をそのままプログラムに落とし込むには少しサイズが大きいようですね。なので最初の作業は仕様を TODO リスト に分解する作業から着手することにしましょう。仕様をどのように TODO に分解していくかは 50 分でわかるテスト駆動開発の 26 分あたりを参考にしてください。

TODO リスト

  • 数を文字列にして返す

  • 3 の倍数のときは数の代わりに「Fizz」と返す

  • 5 の倍数のときは「Buzz」と返す

  • 3 と 5 両方の倍数の場合には「FizzBuzz」と返す

  • 1 から 100 までの数

  • プリントする

まず 数を文字列にして返す作業に取り掛かりたいのですがまだプログラミング対象としてはサイズが大きいようですね。もう少し具体的に分割しましょう。

  • 数を文字列にして返す

    • 1 を渡したら文字列”1”を返す

これならプログラムの対象として実装できそうですね。

テストファーストから始めるテスト駆動開発

テストファースト

最初にプログラムする対象を決めたので早速プロダクトコードを実装・・・ではなく テストファースト で作業を進めていきましょう。まずはプログラムを実行するための準備作業を進める必要がありますね。

テストファースト

いつテストを書くべきだろうか—-それはテスト対象のコードを書く前だ。

— テスト駆動開発

では、どうやってテストすればいいでしょうか?テスティングフレームワークを使って自動テストを書きましょう。

テスト(名詞) どうやってソフトウェアをテストすればよいだろか—-自動テストを書こう。

— テスト駆動開発

今回 Ruby のテスティングフレームワークには Minitestを利用します。Minitest の詳しい使い方に関しては Minitest の基本 6を参照してください。では、まず以下の内容のテキストファイルを作成して main.rb で保存します。

1
2
3
4
5
6
7
8
9
10
11
12
13
require 'minitest/reporters'
Minitest::Reporters.use!
require 'minitest/autorun'

class HelloTest < Minitest::Test
def test_greeting
assert_equal 'hello world', greeting
end
end

def greeting
'hello world'
end

テストを実行します。

1
2
3
4
5
$ ruby main.rb
Traceback (most recent call last):
2: from main.rb:2:in `<main>'
1: from /home/gitpod/.rvm/rubies/ruby-2.5.5/lib/ruby/site_ruby/2.5.0/rubygems/core_ext/kernel_require.rb:54:in `require'
/home/gitpod/.rvm/rubies/ruby-2.5.5/lib/ruby/site_ruby/2.5.0/rubygems/core_ext/kernel_require.rb:54:in `require': cannot load such file -- minitest/reporters (LoadError)

おおっと!いきなりエラーが出てきましたね。でも落ち着いてください。まず最初にやることはエラーメッセージの内容を読むことです。ここでは require': cannot load such file — minitest/reporters (LoadError) と表示されています。取っ掛かりとしては エラーメッセージをキーワードに検索をする というのがあります。ちなみにここでは minitest/reporters という Gem がインストールされていなかったため読み込みエラーが発生していたようです。サイトの Installation を参考に Gem をインストールしておきましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ gem install minitest-reporters
Fetching minitest-reporters-1.4.2.gem
Fetching ansi-1.5.0.gem
Fetching builder-3.2.4.gem
Successfully installed ansi-1.5.0
Successfully installed builder-3.2.4
Successfully installed minitest-reporters-1.4.2
Parsing documentation for ansi-1.5.0
Installing ri documentation for ansi-1.5.0
Parsing documentation for builder-3.2.4
Installing ri documentation for builder-3.2.4
Parsing documentation for minitest-reporters-1.4.2
Installing ri documentation for minitest-reporters-1.4.2
Done installing documentation for ansi, builder, minitest-reporters after 3 seconds
3 gems installed

Gem のインストールが完了したので再度実行してみましょう。今度はうまくいったようですね。Gem って何?と思ったかもしれませんがここでは Ruby の外部プログラム部品のようなものだと思っておいてください。minitest-reporters というのはテスト結果の見栄えを良くするための追加外部プログラムです。先程の作業ではそれを gem install コマンドでインストールしたのです。

1
2
3
4
5
6
7
$ ruby main.rb
Started with run options --seed 9701

1/1: [======================================================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00090s
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips

テストは成功しましたね。では続いてテストを失敗させてみましょう。hello worldhello world!!! に書き換えてテストを実行してみるとどうなるでしょうか。

1
2
3
4
5
6
7
...
class HelloTest < Minitest::Test
def test_greeting
assert_equal 'hello world!!!', greeting
end
end
...
1
2
3
4
5
6
7
8
9
10
11
12
13
$ ruby main.rb
Started with run options --seed 18217

FAIL["test_greeting", #<Minitest::Reporters::Suite:0x00007f98a59194f8 @name="HelloTest">, 0.0007280000027094502]
test_greeting#HelloTest (0.00s)
Expected: "hello world!!!"
Actual: "hello world"
main.rb:11:in `test_greeting'

1/1: [======================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00101s
1 tests, 1 assertions, 1 failures, 0 errors, 0 skips

オッケー、テスティングフレームワークが正常に読み込まれて動作することが確認できました。テストが正常に通るように戻しておきましょう。続いてバージョン管理システムのセットアップをしておきましょう。バージョン管理システム何それ?だって!?君はセーブしないでロールプレイングゲームをクリアできるのか?できないならまず ここで Git を使ったバージョン管理の基本を学んでおきましょう。

1
2
3
$ git init
$ git add .
$ git commit -m 'test: セットアップ'

これでソフトウェア開発の三種の神器のうち バージョン管理テスティング の準備が整いましたので TODO リスト の最初の作業に取り掛かかるとしましょう。

仮実装

TODO リスト

  • 数を文字列にして返す

    • 1 を渡したら文字列”1”を返す
  • 3 の倍数のときは数の代わりに「Fizz」と返す

  • 5 の倍数のときは「Buzz」と返す

  • 3 と 5 両方の倍数の場合には「FizzBuzz」と返す

  • 1 から 100 までの数

  • プリントする

1 を渡したら文字列”1”を返す プログラムを main.rb に書きましょう。最初に何を書くのかって?
アサーションを最初に書きましょう。

アサートファースト

いつアサーションを書くべきだろうか—-最初に書こう

  • システム構築はどこから始めるべきだろうか。システム構築が終わったらこうなる、というストーリーを語るところからだ。

  • 機能はどこから書き始めるべきだろうか。コードが書き終わったらこのように動く、というテストを書くところからだ。

  • ではテストはどこから書き始めるべきだろうか。それはテストの終わりにパスすべきアサーションを書くところからだ。

— テスト駆動開発

まず、セットアッププログラムは不要なので削除しておきましょう。

1
2
3
require 'minitest/reporters'
Minitest::Reporters.use!
require 'minitest/autorun'

テストコードを書きます。え?日本語でテストケースを書くの?ですかって。開発体制にもよりますが日本人が開発するのであれば無理に英語で書くよりドキュメントとしての可読性が上がるのでテストコードであれば問題は無いと思います。

テストコードを読みやすくするのは、テスト以外のコードを読みやすくするのと同じくらい大切なことだ。

— リーダブルコード

1
2
3
4
5
6
7
8
9
require 'minitest/reporters'
Minitest::Reporters.use!
require 'minitest/autorun'

class FizzBuzzTest < Minitest::Test
def test_1を渡したら文字列1を返す
assert_equal '1', FizzBuzz.generate(1)
end
end

テストを実行します。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ ruby main.rb
Started with run options --seed 678

ERROR["test_1を渡したら文字列1を返す", #<Minitest::Reporters::Suite:0x00007f956d8b6870 @name="FizzBuzzTest">, 0.0006979999998293351]
test_1を渡したら文字列1を返す#FizzBuzzTest (0.00s)
NameError: NameError: uninitialized constant FizzBuzzTest::FizzBuzz
Did you mean? FizzBuzzTest
main.rb:10:in `test_1を渡したら文字列1を返す'

1/1: [======================================================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00201s
1 tests, 0 assertions, 0 failures, 1 errors, 0 skips

NameError: NameError: uninitialized constant FizzBuzzTest::FizzBuzz…​FizzBuzz が定義されていない。そうですねまだ作ってないのだから当然ですよね。ではFizzBuzz::generate メソッドを作りましょう。どんな振る舞いを書けばいいのでしょうか?とりあえず最初のテストを通すために 仮実装 から始めるとしましょう。

仮実装を経て本実装へ

失敗するテストを書いてから、最初に行う実装はどのようなものだろうか—-ベタ書きの値を返そう。

— テスト駆動開発

FizzBuzz クラス を定義して 文字列リテラル を返す FizzBuzz::generate クラスメソッド を作成しましょう。ちょっと何言ってるかわからないかもしれませんがとりあえずそんなものだと思って書いてみてください。

1
2
3
4
5
6
...
class FizzBuzz
def self.generate(n)
'1'
end
end

テストが通ることを確認します。

1
2
3
4
5
6
7
$ ruby main.rb
Started with run options --seed 60122

1/1: [======================================================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00094s
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips

オッケー、これで TODO リストを片付けることができました。え?こんなベタ書きのプログラムでいいの?他に考えないといけないことたくさんあるんじゃない?ばかじゃないの?と思われるかもしませんが、この細かいステップに今しばらくお付き合いいただきたい。

TODO リスト

  • 数を文字列にして返す

    • 1 を渡したら文字列”1”を返す
  • 3 の倍数のときは数の代わりに「Fizz」と返す

  • 5 の倍数のときは「Buzz」と返す

  • 3 と 5 両方の倍数の場合には「FizzBuzz」と返す

  • 1 から 100 までの数

  • プリントする

三角測量

1 を渡したら文字列 1 を返すようにできました。では、2 を渡したらどうなるでしょうか?

TODO リスト

  • 数を文字列にして返す

    • 1 を渡したら文字列”1”を返す

    • 2 を渡したら文字列”2”を返す

  • 3 の倍数のときは数の代わりに「Fizz」と返す

  • 5 の倍数のときは「Buzz」と返す

  • 3 と 5 両方の倍数の場合には「FizzBuzz」と返す

  • 1 から 100 までの数

  • プリントする

1
2
3
4
5
6
7
8
9
10
...
class FizzBuzzTest < Minitest::Test
def test_1を渡したら文字列1を返す
assert_equal '1', FizzBuzz.generate(1)
end

def test_2を渡したら文字列2を返す
assert_equal '2', FizzBuzz.generate(2)
end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
$ ruby main.rb
Started with run options --seed 62350

FAIL["test_2を渡したら文字列2を返す", #<Minitest::Reporters::Suite:0x00007fa4968938d8 @name="FizzBuzzTest">, 0.0009390000013809185]
test_2を渡したら文字列2を返す#FizzBuzzTest (0.00s)
Expected: "2"
Actual: "1"
main.rb:17:in `test_2を渡したら文字列2を返す'

2/2: [======================================================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00179s
2 tests, 2 assertions, 1 failures, 0 errors, 0 skips

テストが失敗しました。それは文字列 1 しか返さないプログラムなのだから当然ですよね。では 1 が渡されたら文字列 1 を返し、2 を渡したら文字列 2 を返すようにプログラムを修正しましょう。数値リテラル文字列リテラル に変換する必要があります。公式リファレンスで調べてみましょう。

Ruby の公式リファレンスは https://docs.ruby-lang.org/ です。日本語リファレンス からるりまサーチを選択してキーワード検索してみましょう。文字列 変換キーワードで検索すると to_s というキーワードが出てきました。今度はto_sで検索すると色々出てきました、どうやら to_s を使えばいいみたいですね。

ちなみに検索エンジンから Ruby 文字列 変換で  検索してもいろいろ出てくるのですがすべてのサイトが必ずしも正確な説明をしているまたは最新のバージョンに対応しているとは限らないので始めは公式リファレンスや市販の書籍から調べる癖をつけておきましょう。

1
2
3
4
5
6
...
class FizzBuzz
def self.generate(n)
n.to_s
end
end

テストを実行します。

1
2
3
4
5
6
7
$ ruby main.rb
Started with run options --seed 42479

2/2: [======================================================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00098s
2 tests, 2 assertions, 0 failures, 0 errors, 0 skips

テストが無事通りました。このように2つ目のテストによって FizzBuzz::generate メソッドの一般化を実現することができました。このようなアプローチを 三角測量 と言います。

三角測量

テストから最も慎重に一般化を引き出すやり方はどのようなものだろうか—-2つ以上の例があるときだけ、一般化を行うようにしよう。

— テスト駆動開発

TODO リスト

  • 数を文字列にして返す

    • 1 を渡したら文字列”1”を返す

    • 2 を渡したら文字列”2”を返す

  • 3 の倍数のときは数の代わりに「Fizz」と返す

  • 5 の倍数のときは「Buzz」と返す

  • 3 と 5 両方の倍数の場合には「FizzBuzz」と返す

  • 1 から 100 までの数

  • プリントする

たかが 数を文字列にして返す プログラムを書くのにこんなに細かいステップを踏んでいくの?と思ったかもしれません。プログラムを書くということは細かいステップを踏んで行くことなのです。そして、細かいステップを踏み続けることが大切なことなのです。

TDD で大事なのは、細かいステップを踏むことではなく、細かいステップを踏み続けられるようになることだ。

— テスト駆動開発

あと、テストケースの内容がアサーション一行ですがもっと検証するべきことがあるんじゃない?と思うでしょう。検証したいことがあれば独立したテストケースを追加しましょう。このような書き方はよろしくありません。

1
2
3
4
5
6
7
8
9
...
def test_数字を渡したら文字列を返す
assert_equal '1', FizzBuzz.generate(1)
assert_equal '2', FizzBuzz.generate(2)
assert_equal '3', FizzBuzz.generate(3)
assert_equal '4', FizzBuzz.generate(4)
assert_equal '5', FizzBuzz.generate(5)
end
...

テストの本質というのは、「こういう状況と入力から、こういう振る舞いと出力を期待する」のレベルまで要約できる。

— リーダブルコード

ここで一段落ついたので、これまでの作業内容をバージョン管理システムにコミットしておきましょう。

1
2
$ git add main.rb
$ git commit -m 'test: 数を文字列にして返す'

リファクタリングから始めるテスト駆動開発

リファクタリング

ここでテスト駆動開発の流れを確認しておきましょう。

  1. レッド:動作しない、おそらく最初のうちはコンパイルも通らないテストを1つ書く。

  2. グリーン:そのテストを迅速に動作させる。このステップでは罪を犯してもよい。

  3. リファクタリング:テストを通すために発生した重複をすべて除去する。

レッド・グリーン・リファクタリング。それが TDD のマントラだ。

— テスト駆動開発

コードはグリーンの状態ですが リファクタリング を実施していませんね。重複を除去しましょう。

リファクタリング(名詞) 外部から見たときの振る舞いを保ちつつ、理解や修正が簡単になるように、ソフトウェアの内部構造を変化させること。

— リファクタリング(第 2 版)

リファクタリングする(動詞) 一連のリファクタリングを適用して、外部から見た振る舞いの変更なしに、ソフトウェアを再構築すること。

— リファクタリング(第 2 版

メソッドの抽出

テストコードを見てください。テストを実行するにあたって毎回前準備を実行する必要があります。こうした処理は往々にして同じ処理を実行するものなので
メソッドの抽出 を適用して重複を除去しましょう。

メソッドの抽出

ひとまとめにできるコードの断片がある。

コードの断片をメソッドにして、それを目的を表すような名前をつける。

— 新装版 リファクタリング

1
2
3
4
5
6
7
8
9
class FizzBuzzTest < Minitest::Test
def test_1を渡したら文字列1を返す
assert_equal '1', FizzBuzz.generate(1)
end

def test_2を渡したら文字列2を返す
assert_equal '2', FizzBuzz.generate(2)
end
end

テストフレームワークでは前処理にあたる部分を実行する機能がサポートされています。Minitest では setup メソッドがそれに当たるので FizzBuzz オブジェクトを共有して共通利用できるようにしてみましょう。ここでは インスタンス変数FizzBuzz クラス の参照を 代入 して各テストメソッドで共有できるようにしました。ちょっと何言ってるかわからないかもしれませんがここではそんなことをやってるぐらいのイメージで大丈夫です。

1
2
3
4
5
6
7
8
9
10
11
12
13
class FizzBuzzTest < Minitest::Test
def setup
@fizzbuzz = FizzBuzz
end

def test_1を渡したら文字列1を返す
assert_equal '1', @fizzbuzz.generate(1)
end

def test_2を渡したら文字列2を返す
assert_equal '2', @fizzbuzz.generate(2)
end
end

テストプログラムを変更してしまいましたが壊れていないでしょうか?確認するにはどうすればいいでしょう? テストを実行して確認すればいいですよね。

1
2
3
4
5
6
7
$ ruby main.rb
Started with run options --seed 33356

2/2: [======================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00083s
2 tests, 2 assertions, 0 failures, 0 errors, 0 skips

オッケー、前回コミットした時と同じグリーンの状態のままですよね。区切りが良いのでここでコミットしておきましょう。

1
2
$ git add main.rb
$ git commit -m 'refactor: メソッドの抽出'

変数名の変更

もう一つ気になるところがあります。

1
2
3
4
5
6
...
class FizzBuzz
def self.generate(n)
n.to_s
end
end

引数の名前が n ですね。コンピュータにはわかるかもしれませんが人間が読むコードとして少し不親切です。特に Ruby のような動的言語では型が明確に定義されないのでなおさらです。ここは 変数名の変更 を適用して人間にとって読みやすいコードにリファクタリングしましょう。

コンパイラがわかるコードは誰にでも書ける。すぐれたプログラマは人間にとってわかりやすいコードを書く。

— リファクタリング(第 2 版)

名前は短いコメントだと思えばいい。短くてもいい名前をつければ、それだけ多くの情報を伝えることができる。

— リーダブルコード

1
2
3
4
5
6
...
class FizzBuzz
def self.generate(number)
number.to_s
end
end

続いて、変更で壊れていないかを確認します。

1
2
3
4
5
6
7
$ ruby main.rb
Started with run options --seed 33356

2/2: [======================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00083s
2 tests, 2 assertions, 0 failures, 0 errors, 0 skips

オッケー、この時点でテストコードとプロダクトコードを変更しましたがその変更はすでに作成した自動テストによって壊れていないことを簡単に確認することができました。え、こんな簡単な変更でプログラムが壊れるわけないじゃん、ドジっ子なの?ですって。残念ながら私は絶対ミスしない完璧な人間ではないし、どちらかといえば注意力の足りないプログラマなのでこんな間違いも普通にやらかします。

1
2
3
4
5
6
...
class FizzBuzz
def self.generate(number)
numbr.to_s
end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ ruby main.rb
Started with run options --seed 59453

ERROR["test_1を渡したら文字列1を返す", #<Minitest::Reporters::Suite:0x0000564f6b1dfc70 @name="FizzBuzzTest">, 0.001019135997921694]
test_1を渡したら文字列1を返す#FizzBuzzTest (0.00s)
NameError: NameError: undefined local variable or method `numbr' for FizzBuzz:Class
Did you mean? number
main.rb:21:in `generate'
main.rb:11:in `test_1を渡したら文字列1を返す'

ERROR["test_2を渡したら文字列2を返す", #<Minitest::Reporters::Suite:0x0000564f6b1985f0 @name="FizzBuzzTest">, 0.003952859999117209]
test_2を渡したら文字列2を返す#FizzBuzzTest (0.00s)
NameError: NameError: undefined local variable or method `numbr' for FizzBuzz:Class
Did you mean? number
main.rb:21:in `generate'
main.rb:15:in `test_2を渡したら文字列2を返す'

2/2: [====================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00746s
2 tests, 0 assertions, 0 failures, 2 errors, 0 skips

最初にプロダクトコードを書いて一通りの機能を作ってから動作を確認する進め方だとこの手の間違いはいつどこで作り込んだのかわからなくなるため原因の調査に時間がかかり残念な経験をしたドジっ子プログラマは変更なんてするもんじゃないと思いコードを変更することに不安を持つようになるでしょう。でも、テスト駆動開発ならそんなドジっ子プログラマでも自動テストと小さなステップのおかげで上記のようなしょうもない間違いもすぐに見つけてすぐに対応することができるのでコードを変更する勇気を持つことができるのです。

テスト駆動開発は、プログラミング中の不安をコントロールする手法だ。

— テスト駆動開発

リファクタリングでは小さなステップでプログラムを変更していく。そのため間違ってもバグを見つけるのは簡単である。

— リファクタリング(第 2 版)

このグリーンの状態にいつでも戻れるようにコミットして次の TODO リスト の内容に取り掛かるとしましょう。

1
2
$ git add main.rb
$ git commit -m 'refactor: 変数名の変更'

リファクタリングが成功するたびにコミットしておけば、たとえ壊してしまったとしても、動いていた状態に戻すことができます。変更をコミットしておき、意味のある単位としてまとまってから、共有のリポジトリに変更をプッシュすればよいのです。

— リファクタリング(第 2 版)

明白な実装

次は 3 を渡したら文字列”Fizz” を返すプログラムに取り組むとしましょう。

TODO リスト

  • 数を文字列にして返す

    • 1 を渡したら文字列”1”を返す

    • 2 を渡したら文字列”2”を返す

  • 3 の倍数のときは数の代わりに「Fizz」と返す

    • 3 を渡したら文字列”Fizz”を返す
  • 5 の倍数のときは「Buzz」と返す

  • 3 と 5 両方の倍数の場合には「FizzBuzz」と返す

  • 1 から 100 までの数

  • プリントする

まずは、テストファースト アサートファースト で小さなステップで進めていくんでしたよね。

1
2
3
4
5
...
def test_3を渡したら文字列Fizzを返す
assert_equal 'Fizz', @fizzbuzz.generate(3)
end
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ ruby main.rb
Started with run options --seed 7095

FAIL["test_3を渡したら文字列Fizzを返す", #<Minitest::Reporters::Suite:0x00007fbadf865f50 @name="FizzBuzzTest">, 0.017029999995429534]
test_3を渡したら文字列Fizzを返す#FizzBuzzTest (0.02s)
--- expected
+++ actual
@@ -1 +1,3 @@
-"Fizz"
+# encoding: US-ASCII
+# valid: true
+"3"
main.rb:19:in `test_3を渡したら文字列Fizzを返す'

3/3: [======================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.05129s
3 tests, 3 assertions, 1 failures, 0 errors, 0 skips

さて、失敗するテストを書いたので次はテストを通すためのプロダクトコードを書くわけですがどうしましょうか? 仮実装 でベタなコードを書きますか?実現したい振る舞いはもし3を渡したらならば文字列Fizzを返す です。英語なら If number is 3, result is Fizzといったところでしょうか。ここは 明白な実装 で片付けた方が早いでしょう。

明白な実装

シンプルな操作を実現するにはどうすればいいだろうか—-そのまま実装しよう。

仮実装や三角測量は、細かく細かく刻んだ小さなステップだ。だが、ときには実装をどうすべきか既に見えていることが。
そのまま進もう。例えば先ほどの plus メソッドくらいシンプルなものを仮実装する必要が本当にあるだろうか。
普通は、その必要はない。頭に浮かんだ明白な実装をただ単にコードに落とすだけだ。もしもレッドバーが出て驚いたら、あらためてもう少し歩幅を小さくしよう。

— テスト駆動開発

1
2
3
4
5
class FizzBuzz
def self.generate(number)
number.to_s
end
end

ここでは if 式演算子 を使ってみましょう。なんかプログラムっぽくなってきましたね。
3 で割で割り切れる場合は Fizz を返すということは 数値リテラル 3 で割った余りが 0 の場合は 文字列リテラル Fizz を返すということなので余りを求める 演算子 を調べる必要がありますね。公式リファレンスで 算術演算子 をキーワードで検索したところ いろいろ出てきました。 %を使えばいいみたいですね。

1
2
3
4
5
6
7
8
9
class FizzBuzz
def self.generate(number)
result = number.to_s
if number % 3 == 0
result = 'Fizz'
end
result
end
end
1
2
3
4
5
6
7
$ ruby main.rb
Started with run options --seed 37722

3/3: [===========================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00256s
3 tests, 3 assertions, 0 failures, 0 errors, 0 skips

テストがグリーンになったのでコミットしておきます。

1
2
$ git add main.rb
$ git commit -m 'test: 3を渡したら文字列Fizzを返す'

アルゴリズムの置き換え

TODO リスト

  • 数を文字列にして返す

    • 1 を渡したら文字列”1”を返す

    • 2 を渡したら文字列”2”を返す

  • 3 の倍数のときは数の代わりに「Fizz」と返す

    • 3 を渡したら文字列”Fizz”を返す
  • 5 の倍数のときは「Buzz」と返す

    • 5 を渡したら文字列”Buzz”を返す
  • 3 と 5 両方の倍数の場合には「FizzBuzz」と返す

  • 1 から 100 までの数

  • プリントする

1
2
3
4
5
6
7
8
9
class FizzBuzz
def self.generate(number)
result = number.to_s
if number % 3 == 0
result = 'Fizz'
end
result
end
end

レッド・グリーンときたので次はリファクタリングですね。

1
2
3
4
5
6
7
8
9
class FizzBuzz
def self.generate(number)
result = number.to_s
if number.modulo(3).zero?
result = 'Fizz'
end
result
end
end

ここでは アルゴリズムの置き換え を適用します。 メソッドチェーンと述語メソッド を使って Ruby らしい書き方にリファクタリングしてみました。

アルゴリズムの取り替え

アルゴリズムをよりわかりやすいものに置き換えたい。

メソッドの本体を新たなアルゴリズムで置き換える。

— 新装版 リファクタリング

メソッドチェーンは言葉の通り、メソッドを繋げて呼び出す方法です。

— かんたん Ruby

述語メソッドとはメソッド名の末尾に「?」をつけたメソッドのことを指します。

— かんたん Ruby

リファクタリングによりコードが壊れていないかを確認したらコミットしておきましょう。

1
2
3
4
5
6
7
$ ruby main.rb
Started with run options --seed 42180

3/3: [===========================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00501s
3 tests, 3 assertions, 0 failures, 0 errors, 0 skips
1
2
$ git add main.rb
$ git commit -m 'refactor: アルゴリズムの置き換え'

だんだんとリズムに乗ってきました。ここはギアを上げて 明白な実装 で引き続き TODO リスト の内容を片付けていきましょう。

TODO リスト

  • 数を文字列にして返す

    • 1 を渡したら文字列”1”を返す

    • 2 を渡したら文字列”2”を返す

  • 3 の倍数のときは数の代わりに「Fizz」と返す

    • 3 を渡したら文字列”Fizz”を返す
  • 5 の倍数のときは「Buzz」と返す

    • 5 を渡したら文字列”Buzz”を返す
  • 3 と 5 両方の倍数の場合には「FizzBuzz」と返す

  • 1 から 100 までの数

  • プリントする

テストファースト アサートファースト で最初に失敗するテストを書いて

1
2
3
4
5
...
def test_5を渡したら文字列Buzzを返す
assert_equal 'Buzz', @fizzbuzz.generate(5)
end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ ruby main.rb
Started with run options --seed 46876

FAIL["test_5を渡したら文字列Buzzを返す", #<Minitest::Reporters::Suite:0x0000560f86b93700 @name="FizzBuzzTest">, 0.007562776008853689]
test_5を渡したら文字列Buzzを返す#FizzBuzzTest (0.01s)
--- expected
+++ actual
@@ -1 +1,2 @@
-"Buzz"
+# encoding: US-ASCII
+"5"
main.rb:23:in `test_5を渡したら文字列Buzzを返す'

4/4: [===========================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00849s
4 tests, 4 assertions, 1 failures, 0 errors, 0 skips

if/elsif/else 式 を使って条件分岐を追加しましょう。

1
2
3
4
5
6
7
8
9
class FizzBuzz
def self.generate(number)
result = number.to_s
if number.modulo(3).zero?
result = 'Fizz'
end
result
end
end
1
2
3
4
5
6
7
8
9
10
11
class FizzBuzz
def self.generate(number)
result = number.to_s
if number.modulo(3).zero?
result = 'Fizz'
elsif number.modulo(5).zero?
result = 'Buzz'
end
result
end
end
1
2
3
4
5
6
7
$ ruby main.rb
Started with run options --seed 31468

4/4: [===========================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00158s
4 tests, 4 assertions, 0 failures, 0 errors, 0 skips

テストが通ったのでコミットしておきます。

1
2
$ git add main.rb
$ git commit -m 'test: 5を渡したら文字列Buzzを返す'

メソッドのインライン化

TODO リスト

  • 数を文字列にして返す

    • 1 を渡したら文字列”1”を返す

    • 2 を渡したら文字列”2”を返す

  • 3 の倍数のときは数の代わりに「Fizz」と返す

    • 3 を渡したら文字列”Fizz”を返す
  • 5 の倍数のときは「Buzz」と返す

    • 5 を渡したら文字列”Buzz”を返す
  • 3 と 5 両方の倍数の場合には「FizzBuzz」と返す

  • 1 から 100 までの数

  • プリントする

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class FizzBuzzTest < Minitest::Test
def setup
@fizzbuzz = FizzBuzz
end

def test_1を渡したら文字列1を返す
assert_equal '1', @fizzbuzz.generate(1)
end

def test_2を渡したら文字列2を返す
assert_equal '2', @fizzbuzz.generate(2)
end

def test_3を渡したら文字列Fizzを返す
assert_equal 'Fizz', @fizzbuzz.generate(3)
end

def test_5を渡したら文字列Buzzを返す
assert_equal 'Buzz', @fizzbuzz.generate(5)
end
end

まずグループのアウトラインを作ってテストが壊れないかを確認します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class FizzBuzzTest < Minitest::Test
describe 'FizzBuzz' do
describe '三の倍数の場合' do
end

describe '五の倍数の場合' do
end

describe 'その他の場合' do
end
end

def setup
@fizzbuzz = FizzBuzz
end

def test_1を渡したら文字列1を返す
assert_equal '1', @fizzbuzz.generate(1)
end

def test_2を渡したら文字列2を返す
assert_equal '2', @fizzbuzz.generate(2)
end

def test_3を渡したら文字列Fizzを返す
assert_equal 'Fizz', @fizzbuzz.generate(3)
end

def test_5を渡したら文字列Buzzを返す
assert_equal 'Buzz', @fizzbuzz.generate(5)
end
end
1
2
3
4
5
6
7
$ ruby main.rb
Started with run options --seed 39239

4/4: [===========================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00195s
4 tests, 4 assertions, 0 failures, 0 errors, 0 skips

壊れいないことを確認したらセットアップメソッドをまず移動してテストします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class FizzBuzzTest < Minitest::Test
describe 'FizzBuzz' do
def setup
@fizzbuzz = FizzBuzz
end

describe '三の倍数の場合' do
end

describe '五の倍数の場合' do
end

describe 'その他の場合' do
end
end

def test_1を渡したら文字列1を返す
assert_equal '1', @fizzbuzz.generate(1)
end

def test_2を渡したら文字列2を返す
assert_equal '2', @fizzbuzz.generate(2)
end

def test_3を渡したら文字列Fizzを返す
assert_equal 'Fizz', @fizzbuzz.generate(3)
end

def test_5を渡したら文字列Buzzを返す
assert_equal 'Buzz', @fizzbuzz.generate(5)
end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ ruby main.rb
Started with run options --seed 53111

ERROR["test_1を渡したら文字列1を返す", #<Minitest::Reporters::Suite:0x00005603cac658f8 @name="FizzBuzzTest">, 0.0027922230074182153]
test_1を渡したら文字列1を返す#FizzBuzzTest (0.00s)
NoMethodError: NoMethodError: undefined method `generate' for nil:NilClass
main.rb:22:in `test_1を渡したら文字列1を返す'

ERROR["test_3を渡したら文字列Fizzを返す", #<Minitest::Reporters::Suite:0x00005603cac83e98 @name="FizzBuzzTest">, 0.00590475500212051]
test_3を渡したら文字列Fizzを返す#FizzBuzzTest (0.01s)
NoMethodError: NoMethodError: undefined method `generate' for nil:NilClass
main.rb:30:in `test_3を渡したら文字列Fizzを返す'

ERROR["test_5を渡したら文字列Buzzを返す", #<Minitest::Reporters::Suite:0x00005603cac85ec8 @name="FizzBuzzTest">, 0.008002811024198309]
test_5を渡したら文字列Buzzを返す#FizzBuzzTest (0.01s)
NoMethodError: NoMethodError: undefined method `generate' for nil:NilClass
main.rb:34:in `test_5を渡したら文字列Buzzを返す'

ERROR["test_2を渡したら文字列2を返す", #<Minitest::Reporters::Suite:0x00005603cac97e20 @name="FizzBuzzTest">, 0.010200971009908244]
test_2を渡したら文字列2を返す#FizzBuzzTest (0.01s)
NoMethodError: NoMethodError: undefined method `generate' for nil:NilClass
main.rb:26:in `test_2を渡したら文字列2を返す'

4/4: [===========================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.01247s
4 tests, 0 assertions, 0 failures, 4 errors, 0 skips

テストが失敗しました。これは インスタンス変数 @fizzbuzz のスコープから外れたため
FizzBuzz::generate メソッド呼び出しに失敗したようです。テストメソッドを移動して変数のスコープ範囲に入れましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class FizzBuzzTest < Minitest::Test
describe 'FizzBuzz' do
def setup
@fizzbuzz = FizzBuzz
end

describe '三の倍数の場合' do
def test_3を渡したら文字列Fizzを返す
assert_equal 'Fizz', @fizzbuzz.generate(3)
end
end

describe '五の倍数の場合' do
def test_5を渡したら文字列Buzzを返す
assert_equal 'Buzz', @fizzbuzz.generate(5)
end
end

describe 'その他の場合' do
def test_1を渡したら文字列1を返す
assert_equal '1', @fizzbuzz.generate(1)
end

def test_2を渡したら文字列2を返す
assert_equal '2', @fizzbuzz.generate(2)
end
end
end
end

すべてのメソッドを移動したら確認しましょう。

1
2
3
4
5
6
7
$ ruby main.rb
Started with run options --seed 20627

4/4: [===========================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00212s
4 tests, 4 assertions, 0 failures, 0 errors, 0 skips

ここでは、メソッドのインライン化 を適用してしてテストコードを読みやすくすることにしました。テストコードの 自己文書化 により動作する仕様書にすることができました。

メソッドのインライン化

メソッドの本体が、名前をつけて呼ぶまでもなく明らかである。

メソッド本体の呼び出し元にインライン化して、メソッドを除去する

— 新装版 リファクタリング

混乱せずに読めるテストコードを目指すなら(コンピュータではなく人のためにテストを書いていることを忘れてはならない)、テストメソッドの長さは3行を目指そう。

— テスト駆動開発

この関数名は「自己文書化」されている。関数名はいろんなところで使用されるのだから、優れたコメントよりも名前のほうが大切だ。

— リーダブルコード

テストも無事通るようになったのでコミットしておきます。

1
2
$ git add main.rb
$ git commit -m 'refactor: メソッドのインライン化'

さあ、TODO リスト もだいぶ消化されてきましたね。もうひと踏ん張りです。

TODO リスト

  • 数を文字列にして返す

    • 1 を渡したら文字列”1”を返す

    • 2 を渡したら文字列”2”を返す

  • 3 の倍数のときは数の代わりに「Fizz」と返す

    • 3 を渡したら文字列”Fizz”を返す
  • 5 の倍数のときは「Buzz」と返す

    • 5 を渡したら文字列”Buzz”を返す
  • 3 と 5 両方の倍数の場合には「FizzBuzz」と返す

    • 15 を渡したら文字列 FizzBuzz を返す
  • 1 から 100 までの数

  • プリントする

初めに失敗するテストを書きます。

1
2
3
4
5
6
7
...
describe '三と五の倍数の場合' do
def test_15を渡したら文字列FizzBuzzを返す
assert_equal 'FizzBuzz', @fizzbuzz.generate(15)
end
end
...
1
2
3
4
5
6
7
8
9
10
11
12
13
$ ruby main.rb
Started with run options --seed 16335

FAIL["test_15を渡したら文字列FizzBuzzを返す", #<Minitest::Reporters::Suite:0x000056344a3be2a8 @name="FizzBuzz::三と五の倍数の場合">, 0.006737435003742576]
test_15を渡したら文字列FizzBuzzを返す#FizzBuzz::三と五の倍数の場合 (0.01s)
Expected: "FizzBuzz"
Actual: "Fizz"
main.rb:25:in `test_15を渡したら文字列FizzBuzzを返す'

5/5: [===========================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.01347s
5 tests, 5 assertions, 1 failures, 0 errors, 0 skips

続いて先程と同様に条件分岐を追加しましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
class FizzBuzz
def self.generate(number)
result = number.to_s
if number.modulo(3).zero?
result = 'Fizz'
elsif number.modulo(5).zero?
result = 'Buzz'
elsif number.modulo(15).zero?
result = 'FizzBuzz'
end
result
end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ ruby main.rb
Started with run options --seed 45982

FAIL["test_15を渡したら文字列FizzBuzzを返す", #<Minitest::Reporters::Suite:0x00007f822c00b2b0 @name="FizzBuzz::三と五の倍数の場合">, 0.00231200000
0529224]
test_15を渡したら文字列FizzBuzzを返す#FizzBuzz::三と五の倍数の場合 (0.00s)
Expected: "FizzBuzz"
Actual: "Fizz"
main.rb:25:in `test_15を渡したら文字列FizzBuzzを返す'

4/4: [======================================================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00964s
4 tests, 4 assertions, 1 failures, 0 errors, 0 skips

おっと、調子に乗って 明白な実装 をしていたら怒られてしまいました。ここは一旦ギアを下げて小さなステップで何が問題かを調べることにしましょう。

明白な実装はセカンドギアだ。頭で考えていることがうまくコードに落とせないときは、ギアを下げる用意をしよう。

— テスト駆動開発

調べるにあたってコードを頭から読んでもいいのですが、問題が発生したのは 15を渡したら文字列FizzBuzzを返す テストを追加したあとですよね?ということは原因は追加したコードにあるはずですよね?よって、追加部分をデバッグすれば原因をすぐ発見できると思いませんか?

今回は Ruby のデバッガとして Byebug をインストールして使うことにしましょう。

1
$ gem install byebug

インストールが完了したら早速 Byebug からプログラムを起動して動作を確認してみましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ byebug main.rb

[1, 10] in /Users/k2works/Projects/hiroshima-arc/tdd_rb/docs/src/article/code/main.rb
=> 1: require 'minitest/reporters'
2: Minitest::Reporters.use!
3: require 'minitest/autorun'
4:
5: class FizzBuzzTest < Minitest::Test
6: describe 'FizzBuzz' do
7: def setup
8: @fizzbuzz = FizzBuzz
9: end
10:
(byebug)

詳しい操作に関しては print デバッグにさようなら!Ruby 初心者のための Byebug チュートリアルを参照してください。

では、問題の原因を調査するため byebug メソッドでコード内にブレークポイントを埋め込んでデバッガを実行してみましょう。

1
2
3
4
5
6
7
8
9
...
describe '三と五の倍数の場合' do
def test_15を渡したら文字列FizzBuzzを返す
require 'byebug'
byebug
assert_equal 'FizzBuzz', @fizzbuzz.generate(15)
end
end
...
1
2
3
4
5
6
7
8
9
10
11
12
13
$ byebug main.rb

[1, 10] in /Users/k2works/Projects/hiroshima-arc/tdd_rb/docs/src/article/code/main.rb
=> 1: require 'minitest/reporters'
2: Minitest::Reporters.use!
3: require 'minitest/autorun'
4:
5: class FizzBuzzTest < Minitest::Test
6: describe 'FizzBuzz' do
7: def setup
8: @fizzbuzz = FizzBuzz
9: end
10:

ブレークポイントまで continue コマンドで処理を進めます。continue コマンドは c でもいけます。

1
2
3
4
5
6
7
8
9
10
11
(byebug) c
22:
23: describe '三と五の倍数の場合' do
24: def test_15を渡したら文字列FizzBuzzを返す
25: require 'byebug'
26: byebug
=> 27: assert_equal 'FizzBuzz', @fizzbuzz.generate(15)
28: end
29: end
30:
31: describe 'その他の場合' do

続いて問題が発生した @fizzbuzz.generate(15) メソッド内にステップインします。

1
2
3
4
5
6
7
8
9
10
11
(byebug) s
36: end
37: end
38:
39: class FizzBuzz
40: def self.generate(number)
=> 41: result = number.to_s
42: if number.modulo(3).zero?
43: result = 'Fizz'
44: elsif number.modulo(5).zero?
45: result = 'Buzz'

引数の number15 だから elsif number.modulo(15).zero? の行で判定されるはず・・・

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(byebug) s
37: end
38:
39: class FizzBuzz
40: def self.generate(number)
41: result = number.to_s
=> 42: if number.modulo(3).zero?
43: result = 'Fizz'
44: elsif number.modulo(5).zero?
45: result = 'Buzz'
46: elsif number.modulo(15).zero?
(byebug) s
38:
39: class FizzBuzz
40: def self.generate(number)
41: result = number.to_s
42: if number.modulo(3).zero?
=> 43: result = 'Fizz'

ファッ!?

1
2
3
4
5
6
7
   44:     elsif number.modulo(5).zero?
45: result = 'Buzz'
46: elsif number.modulo(15).zero?
47: result = 'FizzBuzz'
(byebug) result
"15"
(byebug) q!

15 は 3 で割り切れるから最初の判定で処理されますよね。まあ、常にコードに注意を払って頭の中で処理しながらコードを書いていればこんなミスすることは無いのでしょうが私はドジっ子プログラマなので計算機ができることは計算機にやらせて間違いがあれば原因を調べて解決するようにしています。とりあえず、テストを通るようにしておきましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...
class FizzBuzz
def self.generate(number)
result = number.to_s
if number.modulo(3).zero?
result = 'Fizz'
if number.modulo(15).zero?
result = 'FizzBuzz'
end
elsif number.modulo(5).zero?
result = 'Buzz'
end
result
end
end
1
2
3
4
5
6
7
$ ruby main.rb
Started with run options --seed 24862

5/5: [===========================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00279s
5 tests, 5 assertions, 0 failures, 0 errors, 0 skips

テストが通ったのでコミットしておきます。コミットログにバグは残らないのですが作業の合間ではバグを作り込んでいましたよね。でも、テストがすぐに教えてくれるのですぐに修正することができました。結果として私のようなドジっ子プログラマでもバグの無いコードを書いているかのように見えるんですよ。

1
2
$ git add main.rb
$ git commit -m 'test: 15を渡したら文字列FizzBuzzを返す'

私はテスト駆動開発を長年行っているので、他人にミスを気づかれる前に、自分の誤りを修正できるだけなのだ。

— テスト駆動開発

先程のコードですが・・・

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...
class FizzBuzz
def self.generate(number)
result = number.to_s
if number.modulo(3).zero?
result = 'Fizz'
if number.modulo(15).zero?
result = 'FizzBuzz'
end
elsif number.modulo(5).zero?
result = 'Buzz'
end
result
end
end

if 式 の中でさらに if 式 をネストしています。いわゆる コードの不吉な臭い がしますね。ここは仕様の文言にある 3と 5 両方の倍数の場合には「FizzBuzz」とプリントすること。 に沿った記述にするとともにネストした部分をわかりやすくするために アルゴリズムの置き換え を適用してリファクタリングをしましょう。

ネストの深いコードは理解しにくい。

— リーダブルコード

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
class FizzBuzz
def self.generate(number)
result = number.to_s
if number.modulo(3).zero? && number.modulo(5).zero?
result = 'FizzBuzz'
elsif number.modulo(3).zero?
result = 'Fizz'
elsif number.modulo(5).zero?
result = 'Buzz'
end
result
end
end

テストして、

1
2
3
4
5
6
7
$ ruby main.rb
Started with run options --seed 48529

5/5: [===========================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00329s
5 tests, 5 assertions, 0 failures, 0 errors, 0 skips

コミットです。

1
2
$ git add main.rb
$ git commit -m 'refactor: アルゴリズムの置き換え'

休憩

TODO リスト

  • 数を文字列にして返す

    • 1 を渡したら文字列”1”を返す

    • 2 を渡したら文字列”2”を返す

  • 3 の倍数のときは数の代わりに「Fizz」と返す

    • 3 を渡したら文字列”Fizz”を返す
  • 5 の倍数のときは「Buzz」と返す

    • 5 を渡したら文字列”Buzz”を返す
  • 3 と 5 両方の倍数の場合には「FizzBuzz」と返す

    • 15 を渡したら文字列 FizzBuzz を返す
  • 1 から 100 までの数

  • プリントする

数を引数にして文字列を返す FizzBuzz::generate メソッドはできたみたいですね。次のやることは・・・新しいメソッドを追加する必要がありそうです。気分を切り替えるため少し休憩を取りましょう。

疲れたり手詰まりになったりしたときはどうすればいいだろうか—-休憩を取ろう。

— テスト駆動開発

引き続き TODO リスト を片付けたいのですが 1から100までの数 を返すプログラムを書かないといけません。3 を渡したら Fizz のような リテラル を返すプログラムではなく 1 から 100 までの 配列オブジェクト を返すようなプログラムにする必要がありそうです。TODO リスト にするとこんな感じでしょうか。

TODO リスト

  • 1 から 100 までの数の配列を返す

    • 配列の初めは文字列の 1 を返す

    • 配列の最後は文字列の 100 を返す

  • プリントする

どうやら 配列オブジェクト を返すプログラムを書かないといけないようですね。え? 明白な実装 の実装イメージがわかない。そんな時はステップを小さくして 仮実装 から始めるとしましょう。

何を書くべきかわかっているときは、明白な実装を行う。わからないときには仮実装を行う。まだ正しい実装が見えてこないなら、三角測量を行う。それでもまだわからないなら、シャワーを浴びに行こう。

— テスト駆動開発

学習用テスト

配列

テストファースト でまず Ruby の 配列 の振る舞いを確認していきましょう。公式リファレンスによると Ruby ではArray クラスとして定義されているようですね。空の配列を作るには [] (配列リテラル)を使えばいいみたいですね。こんな感じかな?

1
2
3
4
5
6
7
8
9
...
describe '1から100までの数の配列を返す' do
def test_配列の初めは文字列の1を返す
result = []
assert_equal '1', result
end
end
end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ ruby main.rb
Started with run options --seed 54004

FAIL["test_配列の初めは文字列の1を返す", #<Minitest::Reporters::Suite:0x00007fd0fb93d540 @name="FizzBuzz::1から
100までの数の配列を返す">, 0.0016740000028221402]
test_配列の初めは文字列の1を返す#FizzBuzz::1から100までの数の配列を返す (0.00s)
Expected: "1"
Actual: []
main.rb:37:in `test_配列の初めは文字列の1を返す'

5/5: [===================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00602s
5 tests, 5 assertions, 1 failures, 0 errors, 0 skips

これは同値ではないのはわかりますね。ではこうしたらどうなるでしょうか?

1
2
3
4
5
6
7
8
9
...
describe '1から100までの数の配列を返す' do
def test_配列の初めは文字列の1を返す
result = ['1']
assert_equal '1', result
end
end
end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
$ ruby main.rb
Started with run options --seed 32701

FAIL["test_配列の初めは文字列の1を返す", #<Minitest::Reporters::Suite:0x00007fb36f096030 @name="FizzBuzz::1から100までの数の配列を返す">, 0.0018850000014936086]
test_配列の初めは文字列の1を返す#FizzBuzz::1から100までの数の配列を返す (0.00s)
Expected: "1"
Actual: ["1"]
main.rb:38:in `test_配列の初めは文字列の1を返す'

5/5: [===================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.04383s
5 tests, 5 assertions, 1 failures, 0 errors, 0 skips

配列 には要素を操作するメソッドが用意されており内容を色々操作できそうですね。でも、いちいちテストコードを編集してテストを実行させるのも面倒なのでここはデバッガを使ってみましょう。まずブレークポイントを設定して・・・

1
2
3
4
5
6
7
8
9
10
11
...
describe '1から100までの数の配列を返す' do
def test_配列の初めは文字列の1を返す
require 'byebug'
byebug
result = ['1']
assert_equal '1', result
end
end
end
end

デバッガを起動します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ byebug main.rb

[1, 10] in /Users/k2works/Projects/hiroshima-arc/tdd_rb/docs/src/article/code/main.rb
=> 1: require 'minitest/reporters'
2: Minitest::Reporters.use!
3: require 'minitest/autorun'
4:
5: class FizzBuzzTest < Minitest::Test
6: describe 'FizzBuzz' do
7: def setup
8: @fizzbuzz = FizzBuzz
9: end
10:
(byebug)

continue でブレークポイントまで進めます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(byebug) c
Started with run options --seed 15764

/0: [=---=---=---=---=---=---=---=---=---=---=---=---=---=---=---=---=---=-] 0% Time: 00:00:00, ETA: ??:??:??
[34, 43] in /Users/k2works/Projects/hiroshima-arc/tdd_rb/docs/src/article/code/main.rb
34:
35: describe '1から100までの数の配列を返す' do
36: def test_配列の初めは文字列の1を返す
37: require 'byebug'
38: byebug
=> 39: result = ['1']
40: assert_equal '1', result
41: end
42: end
43: end

ステップインして result の中身を確認してみましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(byebug) s

[35, 44] in /Users/k2works/Projects/hiroshima-arc/tdd_rb/docs/src/article/code/main.rb
35: describe '1から100までの数の配列を返す' do
36: def test_配列の初めは文字列の1を返す
37: require 'byebug'
38: byebug
39: result = ['1']
=> 40: assert_equal '1', result
41: end
42: end
43: end
44: end
(byebug) result
["1"]

添字を指定して 配列 の最初の文字列を確認してみましょう。

1
2
3
4
(byebug) result
["1"]
(byebug) result[1]
nil

おや?1番目は”1”では無いようですね。配列 は 0 から始まるので 1 番目を指定するにはこうします。

1
2
3
4
5
6
(byebug) result
["1"]
(byebug) result[1]
nil
(byebug) result[0]
"1"

続いて、複数の文字列から構成される 配列 を作ってみましょう。

1
2
3
4
5
6
(byebug) result = ['1','2','3']
["1", "2", "3"]
(byebug) result[0]
"1"
(byebug) result[2]
"3"

ちなみに Ruby だとこのように表記することができます。直感的でわかりやすくないですか?

1
2
3
4
5
6
(byebug) result
["1", "2", "3"]
(byebug) result.first
"1"
(byebug) result.last
"3"

最後に追加、削除、変更をやってみましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(byebug) result = ['1','2','3']
["1", "2", "3"]
(byebug) result << '4'
["1", "2", "3", "4"]
(byebug) result.push('4')
["1", "2", "3", "4", "4"]
(byebug) result.delete_at(3)
"4"
(byebug) result
["1", "2", "3", "4"]
(byebug) result[2] = '30'
"30"
(byebug) result
["1", "2", "30", "4"]

配列 の振る舞いもだいぶイメージできたのでデバッガを終了させてテストコードを少し変えてみましょう。

1
2
(byebug) q
Really quit? (y/n) y
1
2
3
4
5
6
7
8
9
10
11
...
describe '1から100までの数の配列を返す' do
def test_配列の初めは文字列の1を返す
result = ['1', '2', '3']
assert_equal '1', result.first
assert_equal '2', result[1]
assert_equal '3', result.last
end
end
end
end
1
2
3
4
5
6
7
$ ruby main.rb
Started with run options --seed 39118

5/5: [===================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00186s
5 tests, 7 assertions, 0 failures, 0 errors, 0 skips

変数 result に配列を返すメソッドを作れば良さそうですね。とりあえずメソッド名は今の時点ではあまり考えずに・・・

1
2
3
4
5
6
7
8
9
...
describe '1から100までの数の配列を返す' do
def test_配列の初めは文字列の1を返す
result = FizzBuzz.print_1_to_100
assert_equal '1', result.first
end
end
end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
$ ruby main.rb
Started with run options --seed 19247

ERROR["test_配列の初めは文字列の1を返す", #<Minitest::Reporters::Suite:0x00007faaea925058 @name="FizzBuzz::1から
100までの数の配列を返す">, 0.0017889999980980065]
test_配列の初めは文字列の1を返す#FizzBuzz::1から100までの数の配列を返す (0.00s)
NoMethodError: NoMethodError: undefined method `print_1_to_100' for FizzBuzz:Class
main.rb:37:in `test_配列の初めは文字列の1を返す'

5/5: [===================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00454s
5 tests, 4 assertions, 0 failures, 1 errors, 0 skips

ここまでくれば 仮実装 はできますね。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class FizzBuzz
def self.generate(number)
result = number.to_s
if number.modulo(3).zero? && number.modulo(5).zero?
result = 'FizzBuzz'
elsif number.modulo(3).zero?
result = 'Fizz'
elsif number.modulo(5).zero?
result = 'Buzz'
end
result
end

def self.print_1_to_100
[1, 2, 3]
end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ ruby main.rb
Started with run options --seed 24564

FAIL["test_配列の初めは文字列の1を返す", #<Minitest::Reporters::Suite:0x00007fefd8917060 @name="FizzBuzz::1から
100までの数の配列を返す">, 0.0011969999977736734]
test_配列の初めは文字列の1を返す#FizzBuzz::1から100までの数の配列を返す (0.00s)
Expected: "1"
Actual: 1
main.rb:38:in `test_配列の初めは文字列の1を返す'

5/5: [===================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00209s
5 tests, 5 assertions, 1 failures, 0 errors, 0 skips

ファッ!?、ああ、数字ではなく文字列で返すのだからこうですね。

1
2
3
4
5
...
def self.print_1_to_100
['1', '2', '3']
end
end

%記法 を使うとより Ruby らしく書けます。

1
2
3
4
5
...
def self.print_1_to_100
%w[1 2 3]
end
end

%記法とは、文字列や正規表現などを定義する際に、%を使った特別な書き方をすることでエスケープ文字を省略するなど、可読性を高めることができる記法です。

— かんたん Ruby

1
2
3
4
5
6
7
$ ruby main.rb
Started with run options --seed 42995

5/5: [===================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00195s
5 tests, 5 assertions, 0 failures, 0 errors, 0 skips

TODO リスト の1つ目を 仮実装 で片づけことができました。ちなみにテストコードを使ってソフトウェアの振る舞いを検証するテクニックを 学習用テスト と言います。

学習用テスト

チーム外の誰かが書いたソフトウェアのテストを書くのはどのようなときか—-そのソフトウェアの新機能を初めて使う際に書いてみよう。

— テスト駆動開発

TODO リスト

  • 1 から 100 までの数の配列を返す

    • 配列の初めは文字列の 1 を返す

    • 配列の最後は文字列の 100 を返す

  • プリントする

繰り返し処理

FizzBuzz::print_1_to_100 メソッドはまだ最後の要素が検証されていませんね。三角測量 を使って小さなステップで進めていくことにしましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
describe '1から100までの数の配列を返す' do
def test_配列の初めは文字列の1を返す
result = FizzBuzz.print_1_to_100
assert_equal '1', result.first
end

def test_配列の最後は文字列の100を返す
result = FizzBuzz.print_1_to_100
assert_equal '100', result.last
end
end
end
end
1
2
3
4
5
6
7
8
9
10
11
12
$ ruby main.rb
Started with run options --seed 12031

FAIL["test_配列の最後は文字列の100を返す", #<Minitest::Reporters::Suite:0x00007fccc9828500 @name="FizzBuzz::1から100までの数の配列を返す">, 0.0018540000019129366]
test_配列の最後は文字列の100を返す#FizzBuzz::1から100までの数の配列を返す (0.00s)
Expected: "100"
Actual: "3"
main.rb:43:in `test_配列の最後は文字列の100を返す'

6/6: [===================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.02936s

配列は 3 までなので想定通りテストは失敗します。さて、1 から 100 までの文字列で構成される配列をどうやって作りましょうか? 先程は if 式 を使って 条件分岐 をプログラムで実行しました。今回は 繰り返し処理 をプログラムで実行する必要がありそうですね。Ruby の繰り返し処理には for 式 while/until/loop などがありますが実際のところ each メソッド を使った繰り返し処理が主流です。とはいえ、実際に動かして振る舞いを確認しないとイメージは難しいですよね。 学習用テスト を書いてもいいのですが今回は irb 上で簡単なコードを動かしてみる6ことで振る舞いを検証してみましょう。まずコマンドラインでirbを起動します。

Ruby には for 文はあります。ですが、ほとんどの Ruby プログラマは for 文を使いません。筆者も 5〜6 年 Ruby を使っていますが、for 文を書いたことは一度もありません。Ruby の場合は for のような構文で繰り返し処理をさせるのではなく、配列自身に対して「繰り返せ」という命令を送ります。ここで登場するのが each メソッドです。

— プロを目指す人のための Ruby 入門

1
2
$ irb
irb(main):001:0>

まず先程デバッガで検証した配列の作成をやってみましょう。

1
2
irb(main):001:0> result = %w[1 2 3]
=> ["1", "2", "3"]

配列の each メソッドをつかって配列の中身を繰り返し処理で表示させてみましょう。p はプリントメソッドです。

1
2
3
4
5
irb(main):003:0> result.each do |n| p n end
"1"
"2"
"3"
=> ["1", "2", "3"]

配列の中身を繰り返し処理で取り出す方法はわかりました。あとは 100 までの配列をどうやって作ればよいのでしょうか?['1','2','3'…​'100']と手書きで作りますか?100 件ぐらいならまあできなくもないでしょうが 1000 件,10000 件ならどうでしょうか?無理ですね。計算機にやってもらいましょう、調べてみると Ruby には レンジオブジェクト(Range) というもの用意されいるそうです。説明を読んでもピンと来ないので実際に動作を確認してみましょう。

レンジオブジェクト(範囲オブジェクトとも呼ばれます)は Range クラスのオブジェクトのことで、「..」や「…​」演算子を使って定義します。「1..3」のように定義し、主に整数値や文字列を使って範囲を表現します。

— かんたん Ruby

1
2
3
4
5
6
7
8
9
10
11
12
irb(main):008:0> (1..5).each do |n| p n end
1
2
3
4
5
=> 1..5
irb(main):009:0> (1...5).each do |n| p n end
1
2
3
4

100 まで表示したいのでこうですね。

1
2
3
4
5
6
7
8
irb(main):010:0> (1..100).each do |n| p n end
1
2
3
..
99
100
=> 1..100

FizzBuzz::print_1_to_100 メソッド明白な実装 イメージができましたか? irb を終了させてプロダクトコードを変更しましょう。

1
irb(main):011:0> exit
1
2
3
4
5
6
7
8
9
...
def self.print_1_to_100
result = []
(1..100).each do |n|
result << n
end
result
end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ ruby main.rb
Started with run options --seed 38412

FAIL["test_配列の初めは文字列の1を返す", #<Minitest::Reporters::Suite:0x00007f858480edf8 @name="FizzBuzz::1から
100までの数の配列を返す">, 0.0012219999989611097]
test_配列の初めは文字列の1を返す#FizzBuzz::1から100までの数の配列を返す (0.00s)
Expected: "1"
Actual: 1
main.rb:38:in `test_配列の初めは文字列の1を返す'

FAIL["test_配列の最後は文字列の100を返す", #<Minitest::Reporters::Suite:0x00007f858480c8f0 @name="FizzBuzz::1から100までの数の配列を返す">, 0.0014040000023669563]
test_配列の最後は文字列の100を返す#FizzBuzz::1から100までの数の配列を返す (0.00s)
Expected: "100"
Actual: 100
main.rb:43:in `test_配列の最後は文字列の100を返す'

6/6: [===================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00218s
6 tests, 6 assertions, 2 failures, 0 errors, 0 skips

ファッ!?また、やらかしました。文字列に変換しなといけませんね。

1
2
3
4
5
6
7
8
9
...
def self.print_1_to_100
result = []
(1..100).each do |n|
result << n.to_s
end
result
end
end
1
2
3
4
5
6
7
$ ruby main.rb
Started with run options --seed 40179

6/6: [===================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00196s
6 tests, 6 assertions, 0 failures, 0 errors, 0 skips

ちなみに、do …​ end を使う代わりに、{}で囲んでもブロックを作れる6のでこのように書き換えることができます。

1
2
3
4
5
6
7
...
def self.print_1_to_100
result = []
(1..100).each { |n| result << n.to_s }
result
end
end

変更したらテストして確認します。

1
2
3
4
5
6
7
$ ruby main.rb
Started with run options --seed 59102

7/7: [===========================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00236s
7 tests, 7 assertions, 0 failures, 0 errors, 0 skips

ここで、一旦コミットしておきましょう。

1
2
$ git add main.rb
$ git commit -m 'test: 1から100までの数を返す'

TODO リスト

  • 1 から 100 までの数の配列を返す

    • 配列の初めは文字列の 1 を返す

    • 配列の最後は文字列の 100 を返す

  • プリントする

メソッド呼び出し

1 から 100 までの数の配列を返すメソッドはできました。しかし、このプログラムは 1 から 100 までの数を FizzBuzz::generate した結果を返すのが正しい振る舞いですよね。 TODO リスト を追加してテストも追加します。

TODO リスト

  • 1 から 100 までの数の配列を返す

    • 配列の初めは文字列の 1 を返す

    • 配列の最後は文字列の 100 を返す

    • 配列の 2 番めは文字列の Fizz を返す

  • プリントする

1
2
3
4
5
6
7
8
...
def test_配列の2番目は文字列のFizzを返す
result = FizzBuzz.print_1_to_100
assert_equal 'Fizz', result[2]
end
end
end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ ruby main.rb
Started with run options --seed 50411

FAIL["test_配列の2番目は文字列のFizzを返す", #<Minitest::Reporters::Suite:0x00007fe8a1917dc8 @name="FizzBuzz::1から100までの数の配列を返す">, 0.01608900000428548]
test_配列の2番目は文字列のをFizz返す#FizzBuzz::1から100までの数の配列を返す (0.02s)
--- expected
+++ actual
@@ -1 +1,3 @@
-"Fizz"
+# encoding: US-ASCII
+# valid: true
+"3"
main.rb:48:in `test_配列の2番目は文字列のFizzを返す'

7/7: [===================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.03112s
7 tests, 7 assertions, 1 failures, 0 errors, 0 skips

ですよね、ここは 繰り返し処理 の中で FizzBuzz::generate を呼び出すように変更しましょう。

1
2
3
4
5
6
7
...
def self.print_1_to_100
result = []
(1..100).each { |n| result << generate(n) }
result
end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
$ ruby main.rb
Started with run options --seed 15549

FAIL["test_配列の最後は文字列の100を返す", #<Minitest::Reporters::Suite:0x00007ff80a907e28 @name="FizzBuzz::1から100までの数の配列を返す">, 0.001347000004898291]
test_配列の最後は文字列の100を返す#FizzBuzz::1から100までの数の配列を返す (0.00s)
Expected: "100"
Actual: "Buzz"
main.rb:43:in `test_配列の最後は文字列の100を返す'

7/7: [===================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00218s
7 tests, 7 assertions, 1 failures, 0 errors, 0 skips

新規に追加したテストはパスしたのですが2つ目のテストが失敗しています。これはテストケースが間違っていますね。

1
2
3
4
5
6
7
8
9
10
11
12
13
...
def test_配列の最後は文字列のBuzzを返す
result = FizzBuzz.print_1_to_100
assert_equal 'Buzz', result.last
end

def test_配列の2番目は文字列のFizzを返す
result = FizzBuzz.print_1_to_100
assert_equal 'Fizz', result[2]
end
end
end
end
1
2
3
4
5
6
7
$ ruby main.rb
Started with run options --seed 21247

7/7: [===================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00217s
7 tests, 7 assertions, 0 failures, 0 errors, 0 skips

他のパターンも明記しておきましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
...
describe '1から100までのFizzBuzzの配列を返す' do
def test_配列の初めは文字列の1を返す
result = FizzBuzz.print_1_to_100
assert_equal '1', result.first
end

def test_配列の最後は文字列のBuzzを返す
result = FizzBuzz.print_1_to_100
assert_equal 'Buzz', result.last
end

def test_配列の2番目は文字列のFizzを返す
result = FizzBuzz.print_1_to_100
assert_equal 'Fizz', result[2]
end

def test_配列の4番目は文字列のBuzzを返す
result = FizzBuzz.print_1_to_100
assert_equal 'Buzz', result[4]
end

def test_配列の14番目は文字列のFizzBuzzを返す
result = FizzBuzz.print_1_to_100
assert_equal 'FizzBuzz', result[14]
end
end
end
end

説明変数 への代入が重複しています。ついでに メソッドの抽出 をして重複をなくしておきましょう。

最初のステップ「準備(Arrange)」は、テスト間で重複しがちだ。それとは対象的に「実行(Act)」「アサート(Assert)」は重複しないことが多い。

— テスト駆動開発

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
...
describe '1から100までのFizzBuzzの配列を返す' do
def setup
@result = FizzBuzz.print_1_to_100
end

def test_配列の初めは文字列の1を返す
assert_equal '1', @result.first
end

def test_配列の最後は文字列のBuzzを返す
assert_equal 'Buzz', @result.last
end

def test_配列の2番目は文字列のFizzを返す
assert_equal 'Fizz', @result[2]
end

def test_配列の4番目は文字列のBuzzを返す
assert_equal 'Buzz', @result[4]
end

def test_配列の14番目は文字列のFizzBuzzを返す
assert_equal 'FizzBuzz', @result[14]
end
end
end
1
2
3
4
5
6
7
$ ruby main.rb
Started with run options --seed 17460

9/9: [===================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00207s
9 tests, 9 assertions, 0 failures, 0 errors, 0 skips

とりあえず、現時点で仕様を満たすプログラムにはなったみたいですね。

1
2
$ git add main.rb
$ git commit -m 'test: 1から100までのFizzBuzzの配列を返す'

TODO リスト

  • 1 から 100 までの FizzBuzz の配列を返す

    • 配列の初めは文字列の 1 を返す

    • 配列の最後は文字列の 100 を返す

    • 配列の 2 番めは文字列の Fizz を返す

    • 配列の 4 番目は文字列の Buzz を返す

    • 配列の 14 番目は文字列の FizzBuzz を返す

  • プリントする

配列や繰り返し処理の理解

まだリファクタリングが残っているのですがその前に Ruby の配列メソッドの理解をもう少し深めたいので 学習用テスト を追加しましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
class FizzBuzzTest < Minitest::Test
describe 'FizzBuzz' do
...
end

describe '配列や繰り返し処理を理解する' do
def test_繰り返し処理
$stdout = StringIO.new
[1, 2, 3].each { |i| p i * i }
output = $stdout.string

assert_equal "1\n" + "4\n" + "9\n", output
end

def test_特定の条件を満たす要素だけを配列に入れて返す
result = [1.1, 2, 3.3, 4].select(&:integer?)
assert_equal [2, 4], result
end

def test_特定の条件を満たす要素だけを配列に入れて返す
result = [1.1, 2, 3.3, 4].find_all(&:integer?)
assert_equal [2, 4], result
end

def test_特定の条件を満たさない要素だけを配列に入れて返す
result = [1.1, 2, 3.3, 4].reject(&:integer?)
assert_equal [1.1, 3.3], result
end

def test_新しい要素の配列を返す
result = %w[apple orange pineapple strawberry].map(&:size)
assert_equal [5, 6, 9, 10], result
end

def test_新しい要素の配列を返す
result = %w[apple orange pineapple strawberry].collect(&:size)
assert_equal [5, 6, 9, 10], result
end

def test_配列の中から条件に一致する要素を取得する
result = %w[apple orange pineapple strawberry].find(&:size)
assert_equal 'apple', result
end

def test_配列の中から条件に一致する要素を取得する
result = %w[apple orange pineapple strawberry].detect(&:size)
assert_equal 'apple', result
end

def test_指定した評価式で並び変えた配列を返す
assert_equal %w[1 10 13 2 3 4], %w[2 4 13 3 1 10].sort
assert_equal %w[1 2 3 4 10 13],
%w[2 4 13 3 1 10].sort { |a, b| a.to_i <=> b.to_i }
assert_equal %w[13 10 4 3 2 1],
%w[2 4 13 3 1 10].sort { |b, a| a.to_i <=> b.to_i }
end

def test_配列の中から条件に一致する要素を取得する
result = %w[apple orange pineapple strawberry apricot].grep(/^a/)
assert_equal %w[apple apricot], result
end

def test_ブロック内の条件式が真である間までの要素を返す
result = [1, 2, 3, 4, 5, 6, 7, 8, 9].take_while { |item| item < 6 }
assert_equal [1, 2, 3, 4, 5], result
end

def test_ブロック内の条件式が真である以降の要素を返す
result = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].drop_while { |item| item < 6 }
assert_equal [6, 7, 8, 9, 10], result
end

def test_畳み込み演算を行う
result = [1, 2, 3, 4, 5].inject(0) { |total, n| total + n }
assert_equal 15, result
end

def test_畳み込み演算を行う
result = [1, 2, 3, 4, 5].reduce { |total, n| total + n }
assert_equal 15, result
end
end
end
1
2
3
4
5
6
7
$ ruby main.rb
Started with run options --seed 18136

19/19: [=================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00307s
19 tests, 21 assertions, 0 failures, 0 errors, 0 skips
1
2
$ git add main.rb
$ git commit -m 'test: 学習用テスト'

コードの不吉な臭い

終わりが見えてきましたがまだリファクタリングの必要がありそうです。

開発を終えるまでに考えつくまでに考えつく限りのテストを書き、テストに支えられたリファクタリングが、網羅性のあるテストに支えられてたリファクタリングになるようにしなければならない。

— テスト駆動開発

ここでプロダクトコードを眺めてみましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class FizzBuzz
def self.generate(number)
result = number.to_s
if number.modulo(3).zero? && number.modulo(5).zero?
result = 'FizzBuzz'
elsif number.modulo(3).zero?
result = 'Fizz'
elsif number.modulo(5).zero?
result = 'Buzz'
end
result
end

def self.print_1_to_100
result = []
(1..100).each { |n| result << generate(n) }
result
end
end

コードの不吉な臭い が漂ってきませんか?私が感じた部分を解説していきますね。

不思議な名前

不思議な名前

明快なコードにするために最も重要なのは、適切な名前付けです。

— リファクタリング(第 2 版)

変数や関数などの構成要素の名前は、抽象的ではなく具体的なものにしよう。

— リーダブルコード

まず、気になったのが print_1_to_100 メソッドです。このメソッドは FizzBuzz の配列を返すメソッドであって 1 から 100 までを表示するメソッドではありませんよね。ここは メソッド名の変更 を適用して処理の内容に沿った名前に変更しましょう。え?動いている処理をわざわざ変更してプログラムを壊す危険を犯す必要があるのかですって。確かに自動テストのない状況でドジっ子プログラマがそんなことをすればいずれ残念なことになるでしょうね。でも、すでに自動テストが用意されている今なら自信をもって動いている処理でも変更できますよね。

リファクタリングに入る前に、しっかりとした一連のテスト群を用意しておくこと。これらのテストには自己診断機能が不可欠である。

— リファクタリング(第 2 版)

テストは不安を退屈に変える賢者の石だ。

— テスト駆動開発

1
2
3
4
5
6
7
...
def self.print_1_to_100
result = []
(1..100).each { |n| result << generate(n) }
result
end
end
1
2
3
4
5
6
7
...
def self.generate_list
result = []
(1..100).each { |n| result << generate(n) }
result
end
end

変更で壊れていないか確認します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ ruby main.rb
Started with run options --seed 47414

ERROR["test_配列の初めは文字列の1を返す", #<Minitest::Reporters::Suite:0x00007fe9e6858108 @name="FizzBuzz::1から
100までのFizzBuzzの配列を返す">, 0.0023099999998521525]
test_配列の初めは文字列の1を返す#FizzBuzz::1から100までのFizzBuzzの配列を返す (0.00s)
NoMethodError: NoMethodError: undefined method `print_1_to_100' for FizzBuzz:Class
main.rb:37:in `setup'
...

ERROR["test_配列の最後は文字列のBuzzを返す", #<Minitest::Reporters::Suite:0x00007fe9f7097160 @name="FizzBuzz::1から100までのFizzBuzzの配列を返す">, 0.011574000000109663]
test_配列の最後は文字列のBuzzを返す#FizzBuzz::1から100までのFizzBuzzの配列を返す (0.01s)
NoMethodError: NoMethodError: undefined method `print_1_to_100' for FizzBuzz:Class
main.rb:37:in `setup'

19/19: [=================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.01479s
19 tests, 16 assertions, 0 failures, 5 errors, 0 skips

いきなり失敗しちゃいました。でも、焦らずエラーメッセージを読みましょう。 NoMethodError: NoMethodError:undefined method `print_1_to_100' for FizzBuzz:Class メソッド名の変更したけどテストは以前のままでしたね。

1
2
3
4
5
6
...
describe '1から100までのFizzBuzzの配列を返す' do
def setup
@result = FizzBuzz.print_1_to_100
end
...
1
2
3
4
5
6
...
describe '1から100までのFizzBuzzの配列を返す' do
def setup
@result = FizzBuzz.generate_list
end
...
1
2
3
4
5
6
7
$ ruby main.rb
Started with run options --seed 54699

19/19: [=================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00351s
19 tests, 21 assertions, 0 failures, 0 errors, 0 skips

プロダクトコードは壊れていなことが確認できたので自信を持ってコミットしておきましょう。

1
2
$ git add main.rb
$ git commit -m 'refactor: メソッド名の変更'

TDD におけるテストの考え方は実用主義に貫かれている。TDD においてテストは目的を達成するための手段であり、その目的は、大きなる自信を伴うコードだ。

— テスト駆動開発

長い関数

長い関数

経験上、長く充実した人生を送るのは、短い関数を持ったプログラムです。

— リファクタリング(第 2 版)

次に気になったのが FizzBuzz::generate メソッド内の if 分岐処理ですね。こうした条件分岐には仕様変更の際に追加ロジックが新たな if 分岐として追加されてどんどん長くなって読みづらいコードに成長する危険性があります。そういうコードは早めに対策を打っておくのが賢明です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class FizzBuzz
def self.generate(number)
result = number.to_s
if number.modulo(3).zero? && number.modulo(5).zero?
result = 'FizzBuzz'
elsif number.modulo(3).zero?
result = 'Fizz'
elsif number.modulo(5).zero?
result = 'Buzz'
end
result
end

def self.generate_list
result = []
(1..100).each { |n| result << generate(n) }
result
end
end

まずコードをもう少し読みやすくしましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class FizzBuzz
def self.generate(number)
result = number.to_s

if number.modulo(3).zero? && number.modulo(5).zero?
result = 'FizzBuzz'
elsif number.modulo(3).zero?
result = 'Fizz'
elsif number.modulo(5).zero?
result = 'Buzz'
end

result
end

def self.generate_list
result = []

(1..100).each { |n| result << generate(n) }

result
end
end

FizzBuzzメソッド は大きく分けて 変数 の初期化 条件分岐 繰り返し処理 による判断、計算そして結果の 代入 を行い最後に 代入 された 変数 を返す流れになっています。 そこで各単位ごとにスペースを挿入してコードの可読性を上げておきましょう。

人間の脳はグループや階層を1つの単位として考える。コードの概要をすばやく把握してもらうには、このような「単位」を作ればいい。

— リーダブルコード

処理の単位ごとに区切りをつけました。次は if 分岐ですがこうします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class FizzBuzz
def self.generate(number)
result = number.to_s

if number.modulo(3).zero? && number.modulo(5).zero?
result = 'FizzBuzz'
elsif number.modulo(3).zero?
result = 'Fizz'
elsif number.modulo(5).zero?
result = 'Buzz'
end

result
end
...
1
2
3
4
5
6
7
8
9
10
11
class FizzBuzz
def self.generate(number)
result = number.to_s

return 'FizzBuzz' if number.modulo(3).zero? && number.modulo(5).zero?
return 'Fizz' if number.modulo(3).zero?
return 'Buzz' if number.modulo(5).zero?

result
end
...
1
2
3
4
5
6
7
$ ruby main.rb
Started with run options --seed 62095

19/19: [=================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00296s
19 tests, 21 assertions, 0 failures, 0 errors, 0 skips

条件に該当した場合は処理を最後まで進めずその場で終了させる書き方を ガード節 と言います。このように書くことで追加ロジックが発生しても既存のコードを編集することなく追加することができるので安全に簡単に変更できるコードにすることができます。

ガード節による入れ子条件記述の置き換え

メソッド内に正常ルートが不明確な条件つき振る舞いがある。

特殊ケースすべてに対してガード節を使う。

— 新装版 リファクタリング

関数で複数の return 文を使ってはいけないと思っている人がいる。アホくさ。関数から早く返すのはいいことだ。むしろ望ましいときもある。

— リーダブルコード

1
2
$ git add main.rb
$ git commit -m 'refactor: ガード節による入れ子条件の置き換え'

どの条件にも該当しない場合は数字を文字列してかえすのですが 一時変数result は最後でしか使われていませんね。このような場合は 変数のインライン化 を適用しましょう。

一時変数のインライン化

簡単な式によって一度だけ代入される一時変数があり、それが他のリファクタリングの障害となっている。

その一時変数への参照をすべて式で置き換える。

— 新装版 リファクタリング

1
2
3
4
5
6
7
8
9
10
11
class FizzBuzz
def self.generate(number)
result = number.to_s

return 'FizzBuzz' if number.modulo(3).zero? && number.modulo(5).zero?
return 'Fizz' if number.modulo(3).zero?
return 'Buzz' if number.modulo(5).zero?

result
end
...
1
2
3
4
5
6
7
8
9
class FizzBuzz
def self.generate(number)
return 'FizzBuzz' if number.modulo(3).zero? && number.modulo(5).zero?
return 'Fizz' if number.modulo(3).zero?
return 'Buzz' if number.modulo(5).zero?

number.to_s
end
...
1
2
3
4
5
6
7
$ ruby main.rb
Started with run options --seed 2528

19/19: [=================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00255s
19 tests, 21 assertions, 0 failures, 0 errors, 0 skips

変更によって壊れていないことが確認できたのでコミットします。

1
2
$ git add main.rb
$ git commit -m 'refactor: 変数のインライン化'

続いて、FizzBuzz を判定する部分ですがもう少しわかりやすくするため 説明用変数の導入 を適用します。

説明用変数の導入

複雑な式がある。

その式の結果または部分的な結果を、その目的を説明する名前をつけた一時変数に代入する。

— リファクタリング(第 2 版)

1
2
3
4
5
6
7
8
9
class FizzBuzz
def self.generate(number)
return 'FizzBuzz' if number.modulo(3).zero? && number.modulo(5).zero?
return 'Fizz' if number.modulo(3).zero?
return 'Buzz' if number.modulo(5).zero?

number.to_s
end
...
1
2
3
4
5
6
7
8
9
10
11
12
class FizzBuzz
def self.generate(number)
is_fizz = number.modulo(3).zero?
is_buzz = number.modulo(5).zero?

return 'FizzBuzz' if number.modulo(3).zero? && number.modulo(5).zero?
return 'Fizz' if is_fizz
return 'Buzz' if is_buzz

number.to_s
end
...

3で割り切れる場合の結果を isFizz 変数に 5 で割り切れる場合の結果 isBuzz 変数に代入して使えるようにしました。このような変数を 説明変数 と呼びます。また似たようなパターンに 要約変数 というものがあります。FizzBuzz を返す判定部分にこの 説明変数 を適用しました。壊れていないか確認しておきましょう。

説明変数

式を簡単に分割するには、式を表す変数を使えばいい。この変数を「説明変数」と呼ぶこともある。

— リーダブルコード

要約変数

大きなコードの塊を小さな名前に置き換えて、管理や把握を簡単にする変数のことを要約変数と呼ぶ。

— リーダブルコード

1
2
3
4
5
6
7
8
9
10
11
12
class FizzBuzz
def self.generate(number)
is_fizz = number.modulo(3).zero?
is_buzz = number.modulo(5).zero?

return 'FizzBuzz' if is_fizz && is_buzz
return 'Fizz' if is_fizz
return 'Buzz' if is_buzz

number.to_s
end
...
1
2
3
4
5
6
7
$ ruby main.rb
Started with run options --seed 4314

19/19: [=================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00262s
19 tests, 21 assertions, 0 failures, 0 errors, 0 skips

壊れていませんね。ではコミットしておきましょう。

1
2
$ git add main.rb
$ git commit -m 'refactor: 変数の抽出'

ループと変更可能なデータ

ループ

プログラミング言語の黎明期から、ループは中心的な存在でした。しかし今ではベルボトムのジーンズやペナントのお土産のように、あまり重要でなくなりつつあります。

— リファクタリング(第 2 版)

FizzBuzz::generate メソッドのリファクタリングはできたので続いて FizzBuzz::generate_list メソッドを見ていきましょう。

1
2
3
4
5
6
7
8
9
...
def self.generate_list
result = []

(1..100).each { |n| result << generate(n) }

result
end
end

空の 配列 を変数に代入してその変数に FizzBuzz::generate メソッドの結果を追加して返す処理ですがもしこのような変更をしてしまったらどうなるでしょうか?

1
2
3
4
5
6
7
8
9
10
...
def self.generate_list
result = []

(1..100).each { |n| result << generate(n) }

result = []
result
end
end
1
2
3
4
5
6
7
8
9
10
11
12
$ ruby main.rb
Started with run options --seed 19180

FAIL["test_配列の14番目は文字列のをFizzBuzz返す", #<Minitest::Reporters::Suite:0x00007fa72805c018 @name="FizzBuzz::1から100までのFizzBuzzの配列を返す">, 0.0021289999967848416]
test_配列の14番目は文字列のをFizzBuzz返す#FizzBuzz::1から100までのFizzBuzzの配列を返す (0.00s)
Expected: "FizzBuzz"
Actual: nil
main.rb:57:in `test_配列の14番目は文字列のをFizzBuzz返す'
...

Finished in 0.03063s
19 tests, 21 assertions, 5 failures, 0 errors, 0 sk

せっかく作った配列を初期化して返してしまいましたね。このようにミュータブルな変数はバグを作り込む原因となる傾向があります。まず一時変数を使わないように変更しましょう。

変更可能なデータ

データの変更はしばしば予期せぬ結果や、厄介なバグを引き起こします。

— リファクタリング(第 2 版)

「永続的に変更されない」変数は扱いやすい。

— リーダブルコード

1
2
3
4
5
...
def self.generate_list
return (1..100).each { |n| result << generate(n) }
end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ ruby main.rb
Started with run options --seed 56578

ERROR["test_配列の4番目は文字列のをBuzz返す", #<Minitest::Reporters::Suite:0x00007fe705854af0 @name="FizzBuzz::1から100までのFizzBuzzの配列を返す">, 0.001975000002857996]
test_配列の4番目は文字列のをBuzz返す#FizzBuzz::1から100までのFizzBuzzの配列を返す (0.00s)
NameError: NameError: undefined local variable or method `result' for FizzBuzz:Class
main.rb:153:in `block in generate_list'
main.rb:153:in `each'
main.rb:153:in `generate_list'
main.rb:37:in `setup'
...
19/19: [=================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.01032s
19 tests, 16 assertions, 0 failures, 5 errors, 0 skips

一時変数 result は使わないので

1
2
3
4
5
...
def self.generate_list
return (1..100).each { |n| generate(n) }
end
end
1
2
3
4
5
6
7
8
9
10
11
12
$ ruby main.rb
Started with run options --seed 35137

ERROR["test_配列の4番目は文字列のをBuzz返す", #<Minitest::Reporters::Suite:0x00007f7f1384ff78 @name="FizzBuzz::1から100までのFizzBuzzの配列を返す">, 0.0014560000017809216]
test_配列の4番目は文字列のをBuzz返す#FizzBuzz::1から100までのFizzBuzzの配列を返す (0.00s)
NoMethodError: NoMethodError: undefined method `[]' for 1..100:Range
main.rb:53:in `test_配列の4番目は文字列のをBuzz返す'
...
19/19: [=================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.03285s
19 tests, 18 assertions, 2 failures, 3 errors, 0 skips

結果を配列にして返したいのですが each メソッド ではうまくできませんね。Ruby には新しい配列を  作成する map メソッド が用意されいるのでそちらを使いましょう。

map は配列の要素を画する際によく利用されるメソッドで、ブロックの最後の要素(メモ)で新しい配列を作ります。

— かんたん Ruby

1
2
3
4
5
...
def self.generate_list
return (1..100).map { |n| generate(n) }
end
end
1
2
3
4
5
6
7
 $ ruby main.rb
Started with run options --seed 44043

19/19: [=================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00261s
19 tests, 21 assertions, 0 failures, 0 errors, 0 skips

うまくいきましたね。あと、Ruby では return を省略できるので

1
2
3
4
5
...
def self.generate_list
(1..100).map { |n| generate(n) }
end
end
1
2
3
4
5
6
$ ruby main.rb
Started with run options --seed 7994

19/19: [=================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00238s

パイプラインによるループの置き換え の適用により each メソッド による繰り返し処理を map メソッド を使ったイミュータブルなコレクションパイプライン処理に変えることができました。

パイプラインによるループの置き換え

多くのプログラマと同様に、私もオブジェクトの集合の反復処理にはループを使うように教えられました。しかし言語環境は、よりすぐれた仕組みとしてコレクションのパイプラインを提供するようになりました。

— リファクタリング(第 2 版)

Ruby に限らず、プログラミングの世界ではしばしばミュータブル(mutable)とイミュータブル(imutable)と言う言葉が登場します。ミュータブルは「変更可能な」という意味で、反対にイミュータブルは「変更できない、不変の」という意味です。

— プロを目指す人のための Ruby 入門

1
2
$ git add main.rb
$ git commit -m 'refactor: パイプラインによるループの置き換え'

マジックナンバー

最大値は 100 にしていますが変更することもあるので マジックナンバーの置き換え を適用してわかりやすくしておきましょう。

シンボル定数によるマジックナンバーの置き換え

特別な意味を持った数字のリテラルがある。

定数を作り、それにふさわしい名前をつけて、そのリテラルを置き換える。

— 新装版 リファクタリング

Ruby では定数は英字の大文字で始まる名前をつけると自動的に定数として扱われます。

1
2
3
4
5
6
7
8
9
class FizzBuzz
MAX_NUMBER = 100

...

def self.generate_list
(1..MAX_NUMBER).map { |n| generate(n) }
end
end

意味のわかる定数として宣言しました。コードに直接記述された 100 をといった 数値リテラル はマジックナンバーと呼ばれ往々にして後で何を意味するものかわからなくなり変更を難しくする原因となります。早めに意味を表す定数にしておきましょう。

名前付けされずにプログラム内に直接記述されている数値をマジックナンバーと呼び、一般的には極力避けるようにします。

— かんたん Ruby

いい名前というのは、変数の目的や値を表すものだ。

— リーダブルコード

1
2
3
4
5
6
7
$ ruby main.rb
Started with run options --seed 32408

19/19: [=================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00241s
19 tests, 21 assertions, 0 failures, 0 errors, 0 skips

テストは通りました。でもこのコードは初見の人には分かりづらいのでコメントを入れておきましょう。Ruby の 単一行コメントアウト のやり方は行頭に # を使います。

1
2
3
4
5
6
...
def self.generate_list
# 1から最大値までのFizzBuzz配列を1発で作る
(1..MAX_NUMBER).map { |n| generate(n) }
end
end

ここではなぜこのような処理を選択したかをコメントしましたが何でもコメントすればよいというわけではありません。

コメント

ここでコメントについて言及しているのは、コメントが消臭剤として使われることがあるからです。コメントが非常に丁寧に書かれているのは、実はわかりにくいコードを補うためだったとうことがよくあるのです。

— リファクタリング(第 2 版)

コメントを書くのであれば、正確に書くべきだ(できるだけ明確で詳細に)。また、コメントには画面の領域を取られるし、読むのにも時間がかかるので、簡潔なものでなければいけない。

— リーダブルコード

1
2
$ git add main.rb
$ git commit -m 'refactor: マジックナンバーの置き換え'

動作するきれいなコード

TODO リスト

  • 数を文字列にして返す

    • 1 を渡したら文字列”1”を返す

    • 2 を渡したら文字列”2”を返す

  • 3 の倍数のときは数の代わりに「Fizz」と返す

    • 3 を渡したら文字列”Fizz”を返す
  • 5 の倍数のときは「Buzz」と返す

    • 5 を渡したら文字列”Buzz”を返す
  • 13 と 5 両方の倍数の場合には「FizzBuzz」と返す

    • 15 を渡したら文字列 FizzBuzz を返す
  • 1 から 100 までの FizzBuzz の配列を返す

    • 配列の初めは文字列の 1 を返す

    • 配列の最後は文字列の 100 を返す

    • 配列の 2 番めは文字列の Fizz を返す

    • 配列の 4 番目は文字列の Buzz を返す

    • 配列の 14 番目は文字列の FizzBuzz を返す

  • プリントする

TODO リスト も残すところあと1つとなりました。これまで main.rb ファイル1つだけで開発を行ってきましたがリリースするにはもうひと手間かけたほうがいいでしょうね。lib ディレクトリを作成したあと main.rb ファイルを fizz_buzz.rb ファイルに名前を変更して lib ディレクトリに移動します。

/
|--lib/
    |
     -- fizz_buzz.rb

続いてテストコードをテストディレクトリに保存してプログラム本体とテストコードを分離します

/
|--lib/
    |
     -- fizz_buzz.rb
|--test/
    |
     -- fizz_buzz_test.rb

分離したテストが動くか確認しておきましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ ruby test/fizz_buzz_test.rb
Started with run options --seed 17134

ERROR["test_1を渡したら文字列1を返す", #<Minitest::Reporters::Suite:0x00007fc07a085060 @name="FizzBuzz::その他の場合">, 0.001282999997783918]
test_1を渡したら文字列1を返す#FizzBuzz::その他の場合 (0.00s)
NameError: NameError: uninitialized constant FizzBuzzTest::FizzBuzz
Did you mean? FizzBuzzTest
test/fizz_buzz_test.rb:8:in `setup'
...
19/19: [===============================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.03717s
19 tests, 12 assertions, 0 failures, 9 errors, 0 skips

テストファイルから FizzBuzz クラスを読み込めるようにする必要があります。

1
2
3
4
5
6
7
require 'minitest/reporters'
Minitest::Reporters.use!
require 'minitest/autorun'
require './lib/fizz_buzz'

class FizzBuzzTest < Minitest::Test
...

Ruby で別のファイルを読み込むには require を使います。

require を使う用途は主に三つあります。

  • 標準添付ライブラリを読み込む

  • 第三者が作成しているライブラリを読み込む

  • 別ファイルに定義した自分のファイルを読み込む

— かんたん Ruby

また、require_relative

という方法も用意されています。どう違うのでしょうか?

require_relative は$LOAD_PATH の参照は行わず「relative」という名称の通り相対的なパスでファイルの読み込みを行います。

— かんたん Ruby

ちょっと何言ってるかわからないうちは require を上記のフォルダ構成で使っていてください。一応以下の使い分けがありますが今は頭の隅に留めるだけでいいと思います。

require は標準添付ライブラリなどの自分が書いていないコードを読み込む時に使い、こちらの require_relative は自分の書いたコードを読み込む時に使うように使い分けるのが良いでしょう。

— かんたん Ruby

1
2
3
4
5
6
7
$ ruby test/fizz_buzz_test.rb
Started with run options --seed 44438

19/19: [=================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00279s
19 tests, 21 assertions, 0 failures, 0 errors, 0 skips

では最後に main.rb ファイルを追加して FizzBuzz:generate_list を呼び出すようにします。

/main.rb
  |--lib/
      |
       -- fizz_buzz.rb
  |--test/
      |
       -- fizz_buzz_test.rb
1
2
3
require './lib/fizz_buzz.rb'

puts FizzBuzz.generate_list

puts は結果を画面に出力するメソッドです。 先程は p メソッドを使って画面に 配列 の中身を1件ずつ表示していましたが今回は 配列 自体を改行して画面に出力するため puts メソッドを使います。機能的にはほどんど変わらないのですが以下の様に使い分けるそうです。

まず、用途としては puts メソッドと print メソッドは一般ユーザ向け、p メソッドは開発者向け、というふうに別かれます。

— プロを目指す人のための Ruby 入門

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ ruby main.rb
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
...
Buzz

ちなみに print メソッドを使った場合はこのように出力されます。

1
2
$ ruby main.rb
["1", "2", "Fizz", "4", "Buzz", "Fizz", "7", "8", "Fizz", "Buzz", "11", "Fizz", "13", "14", "FizzBuzz", "16", "17", "Fizz", "19", "Buzz", "Fizz", "22", "23", "Fizz", "Buzz", "26", "Fizz", "28", "29", "FizzBuzz", "31", "32", "Fizz", "34", "Buzz", "Fizz", "37", "38", "Fizz", "Buzz", "41", "Fizz", "43", "44", "FizzBuzz", "46", "47", "Fizz", "49", "Buzz", "Fizz", "52", "53", "Fizz", "Buzz", "56", "Fizz", "58", "59", "FizzBuzz", "61", "62", "Fizz", "64", "Buzz", "Fizz", "67", "68", "Fizz", "Buzz", "71", "Fizz", "73", "74", "FizzBuzz", "76", "77", "Fizz", "79", "Buzz", "Fizz", "82", "83", "Fizz", "Buzz", "86", "Fizz", "88", "89", "FizzBuzz", "91", "92", "Fizz", "94", "Buzz", "Fizz", "97", "98", "Fizz", "Buzz"] $

プログラムの完成です。コミットしておきましょう。

1
$ git commit -m 'feat: プリントする'

TODO リスト

  • 数を文字列にして返す

    • 1 を渡したら文字列”1”を返す

    • 2 を渡したら文字列”2”を返す

  • 3 の倍数のときは数の代わりに「Fizz」と返す

    • 3 を渡したら文字列”Fizz”を返す
  • 5 の倍数のときは「Buzz」と返す

    • 5 を渡したら文字列”Buzz”を返す
  • 13 と 5 両方の倍数の場合には「FizzBuzz」と返す

    • 15 を渡したら文字列 FizzBuzz を返す
  • 1 から 100 までの FizzBuzz の配列を返す

    • 配列の初めは文字列の 1 を返す

    • 配列の最後は文字列の 100 を返す

    • 配列の 2 番めは文字列の Fizz を返す

    • 配列の 4 番目は文字列の Buzz を返す

    • 配列の 14 番目は文字列の FizzBuzz を返す

  • プリントする

ふりかえり

FizzBuzz プログラムの最初のバージョンをリリースすることができたのでこれまでのふりかえりをしておきましょう。

まず TODO リスト を作成して テストファースト で1つずつ小さなステップで開発を進めていきました。 仮実装を経て本実装へ の過程で Ruby の クラス を定義して 文字列リテラル を返す メソッド を作成しました。この時点で Ruby の オブジェクトとメソッド という概念に触れています。

Ruby の世界では、ほぼどのような値もオブジェクトという概念で表されます。オブジェクトという表現はかなり範囲の広い表現方法で、クラスやインスタンスを含めてオブジェクトと称します。

— かんたん Ruby

プログラミング言語においてメソッド、あるいは関数と呼ばれるものを簡単に説明すると処理をひとかたまりにまとめたものと言って良いでしょう。

— かんたん Ruby

ちょっと何言ってるかわからないかもしれませんが、今はそういう概念があってこうやって書くのねという程度の理解で十分です。

その後 リファクタリング を通じて多くの概念に触れることになりました。 まず 変数名の変更 で Ruby における 変数の概念と操作を通じて名前付けの重要性を学びました。

Ruby では変数を扱うために特別な宣言やキーワードは必要ありません。「=」 の左辺に任意の変数名を記述するだけで変数宣言となります。

— かんたん Ruby

続いて 明白な実装 を通して 制御構造 のうち 条件分岐 のための if 式演算子 を使いプログラムを制御し判定・計算をする方法を学びました。また、アルゴリズムの置き換え を適用してコードをよりわかりやすくしました。

Ruby ではプログラムを構成する最小の要素を式と呼びます。変数やリテラル、制御構文、演算子などが式として扱われます。

— かんたん Ruby

そして、 学習用テスト を通して新しい問題を解決するために 配列オブジェクト レンジオブジェクト といった 文字列リテラル 数値リテラル 以外の データ構造 の使い方を学習して、配列 を操作するための 制御構造 として 繰り返し処理each メソッド を使って実現しました。

これら「100」や「3.14」といった部分を数値リテラルと呼びます。

— かんたん Ruby

このように文字列をシングルクオートやダブルクオートで括っている表記を文字列リテラルと呼びます。

— かんたん Ruby

仕上げは、コードの不吉な臭い からさらなる改善を実施しました。 不思議な名前メソッド自動的テストを用意することで自信を持って リファクタリング を実施し、長い関数 に対して ガード節 を導入し 一時変数 説明変数 など 変数 バリエーションの取り扱いを学びました。そして、ループ変更可能なデータ から コレクションパイプライン の使い方と ミュータブル イミュータブル の概念を学び、コメント のやり方と 定数マジックナンバー の問題を学びました。

最後に、require の使い方を通してファイルの分割方法を学ぶことができました。

ちょっと何言ってるかわからない単語ばかり出てきたかもしれませんがこれで Ruby の基本の半分は抑えています。自分で FizzBuzz コードが書けて用語の意味が説明できるようになれば技能・学科第一段階の半分ぐらいといったところでしょうか。仮免許取得にはまだ習得しなければならない技術と知識がありますので。

良いコード

以下のコードを作成しました。

/main.rb.

1
2
3
require './lib/fizz_buzz.rb'

puts FizzBuzz.generate_list

/lib/fizz_buzz.rb.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class FizzBuzz
MAX_NUMBER = 100

def self.generate(number)
is_fizz = number.modulo(3).zero?
is_buzz = number.modulo(5).zero?

return 'FizzBuzz' if is_fizz && is_buzz
return 'Fizz' if is_fizz
return 'Buzz' if is_buzz

number.to_s
end

def self.generate_list
# 1から最大値までのFizzBuzz配列を1発で作る
(1..MAX_NUMBER).map { |n| generate(n) }
end
end

/test/fizz_buzz_test.rb.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
require 'minitest/reporters'
Minitest::Reporters.use!
require 'minitest/autorun'
require './lib/fizz_buzz'

class FizzBuzzTest < Minitest::Test
describe 'FizzBuzz' do
def setup
@fizzbuzz = FizzBuzz
end

describe '三の倍数の場合' do
def test_3を渡したら文字列Fizzを返す
assert_equal 'Fizz', @fizzbuzz.generate(3)
end
end

describe '五の倍数の場合' do
def test_5を渡したら文字列Buzzを返す
assert_equal 'Buzz', @fizzbuzz.generate(5)
end
end

describe '三と五の倍数の場合' do
def test_15を渡したら文字列FizzBuzzを返す
assert_equal 'FizzBuzz', @fizzbuzz.generate(15)
end
end

describe 'その他の場合' do
def test_1を渡したら文字列1を返す
assert_equal '1', @fizzbuzz.generate(1)
end

def test_2を渡したら文字列2を返す
assert_equal '2', @fizzbuzz.generate(2)
end
end

describe '1から100までのFizzBuzzの配列を返す' do
def setup
@result = FizzBuzz.generate_list
end

def test_配列の初めは文字列の1を返す
assert_equal '1', @result.first
end

def test_配列の最後は文字列のBuzzを返す
assert_equal 'Buzz', @result.last
end

def test_配列の2番目は文字列のFizzを返す
assert_equal 'Fizz', @result[2]
end

def test_配列の4番目は文字列のBuzzを返す
assert_equal 'Buzz', @result[4]
end

def test_配列の14番目は文字列のFizzBuzzを返す
assert_equal 'FizzBuzz', @result[14]
end
end
end

describe '配列や繰り返し処理を理解する' do
def test_繰り返し処理
$stdout = StringIO.new
[1, 2, 3].each { |i| p i * i }
output = $stdout.string

assert_equal "1\n" + "4\n" + "9\n", output
end

def test_特定の条件を満たす要素だけを配列に入れて返す
result = [1.1, 2, 3.3, 4].select(&:integer?)
assert_equal [2, 4], result
end

def test_特定の条件を満たす要素だけを配列に入れて返す
result = [1.1, 2, 3.3, 4].find_all(&:integer?)
assert_equal [2, 4], result
end

def test_特定の条件を満たさない要素だけを配列に入れて返す
result = [1.1, 2, 3.3, 4].reject(&:integer?)
assert_equal [1.1, 3.3], result
end

def test_新しい要素の配列を返す
result = %w[apple orange pineapple strawberry].map(&:size)
assert_equal [5, 6, 9, 10], result
end

def test_新しい要素の配列を返す
result = %w[apple orange pineapple strawberry].collect(&:size)
assert_equal [5, 6, 9, 10], result
end

def test_配列の中から条件に一致する要素を取得する
result = %w[apple orange pineapple strawberry].find(&:size)
assert_equal 'apple', result
end

def test_配列の中から条件に一致する要素を取得する
result = %w[apple orange pineapple strawberry].detect(&:size)
assert_equal 'apple', result
end

def test_指定した評価式で並び変えた配列を返す
assert_equal %w[1 10 13 2 3 4], %w[2 4 13 3 1 10].sort
assert_equal %w[1 2 3 4 10 13],
%w[2 4 13 3 1 10].sort { |a, b| a.to_i <=> b.to_i }
assert_equal %w[13 10 4 3 2 1],
%w[2 4 13 3 1 10].sort { |b, a| a.to_i <=> b.to_i }
end

def test_配列の中から条件に一致する要素を取得する
result = %w[apple orange pineapple strawberry apricot].grep(/^a/)
assert_equal %w[apple apricot], result
end

def test_ブロック内の条件式が真である間までの要素を返す
result = [1, 2, 3, 4, 5, 6, 7, 8, 9].take_while { |item| item < 6 }
assert_equal [1, 2, 3, 4, 5], result
end

def test_ブロック内の条件式が真である以降の要素を返す
result = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].drop_while { |item| item < 6 }
assert_equal [6, 7, 8, 9, 10], result
end

def test_畳み込み演算を行う
result = [1, 2, 3, 4, 5].inject(0) { |total, n| total + n }
assert_equal 15, result
end

def test_畳み込み演算を行う
result = [1, 2, 3, 4, 5].reduce { |total, n| total + n }
assert_equal 15, result
end
end
end

どうでしょう、学習用テストは除くとしてプロダクトコードに対して倍以上のテストコードを作っていますよね。テストコードを作らず一発で fizz_buzz.rb のようなコードを書くことはできますか? たしかに fizz buzz ruby といったキーワードで検索すればサンプルコードは見つかるのでコピーして同じ振る舞いをするコードをすぐに書くことはできるでしょう。でも仕様が追加された場合はどうしましょう。

仕様

1 から 100 までの数をプリントするプログラムを書け。
ただし 3 の倍数のときは数の代わりに「Fizz」と、5 の倍数のときは「Buzz」とプリントし、
3 と 5 両方の倍数の場合には「FizzBuzz」とプリントすること。
タイプごとに出力を切り替えることができる。
タイプ1は通常、タイプ2は数字のみ、タイプ3は FizzBuzz の場合のみをプリントする。

また同じようなコードサンプルを探しますか?私ならば TODO リスト に以下の項目を追加することから始めます。

TODO リスト

  • タイプ 1 の場合

    • 数を文字列にして返す

      • 1 を渡したら文字列”1”を返す

次に何をやるかはもうわかりますよね。テスト駆動開発とはただ失敗するテストを1つずつ書いて通していくことではありません。

TDD は分析技法であり、設計技法であり、実際には開発のすべてのアクティビティを構造化する技法なのだ。

— テスト駆動開発

ではテストファーストで書けば質の高い良いコードがかけるようになるのでしょうか?以下のコードを見てください。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
require 'minitest/reporters'
Minitest::Reporters.use!
require 'minitest/autorun'

class FizzBuzz
# fizz_buzzメソッドを実行する
def self.fizz_buzz(n)
a = n.to_s
if n % 3 == 0
a = 'Fizz'
if n % 15 == 0
a = 'FizzBuzz'
end
elsif n % 5 == 0
a = 'Buzz'
end
a
end

# 1から100までをプリントする
def self.print_1_to_100
n = []
(1..100).each do |i|
n << fizz_buzz(i)
end
n
end
end

class FizzBuzzTest < Minitest::Test
describe 'FizzBuzz' do
def setup
@p = FizzBuzz
end

def test_15を渡したら文字列pを返す
assert_equal 'FizzBuzz', FizzBuzz.fizz_buzz(15)
end
def test_3を渡したら文字列3を返す
assert_equal 'Fizz', FizzBuzz.fizz_buzz(3)
end
def test_1を渡したら文字列1を返す
assert_equal '1', @p.fizz_buzz(1)
end
def test_5を渡したら文字列Buzzを返す
assert_equal 'Buzz', FizzBuzz.fizz_buzz(5)
end

describe '1から100までプリントする' do
def setup
@x = FizzBuzz.print_1_to_100
end

def test_配列の4番目は文字列のをBuzz返す
assert_equal 'Buzz', @x[4]
end

def test_配列の初めは文字列の1を返す
assert_equal '1', @x.first
end

def test_配列の最後は文字列のBuzzを返す
assert_equal 'Buzz', FizzBuzz.print_1_to_100.last
end

def test_配列の14番目は文字列のFizzBuzz返す
assert_equal 'FizzBuzz', @x[14]
end
def test_配列の2番目は文字列の2を返す
assert_equal 'Fizz', @x[2]
end

end
end
end
1
2
3
4
5
6
7
$ ruby test/fizz_buzz_tfd_test.rb
Started with run options --seed 43131

9/9: [===================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00135s
9 tests, 9 assertions, 0 failures, 0 errors, 0 skips

プログラムは動くしテストも通ります。でもこれはテスト駆動開発で作られたと言えるでしょうか?質の高い良いコードでしょうか?何が足りないかはわかりますよね。

テスト駆動開発における質の向上の手段は、リファクタリングによる継続的でインクリメンタルな設計であり、「単なるテストファースト」と「テスト駆動開発」の違いはそこにあります。

— テスト駆動開発 付録 C 訳者解説

そもそも良いコードは何なのでしょうか?いくつかの見解があるようです。

TDD は「より良いコードを書けば、よりうまくいく」という素朴で奇妙な仮設によって成り立っている

— テスト駆動開発

「動作するきれいなコード」。RonJeffries のこの簡潔な言葉が、テスト駆動開発(TDD)のゴールだ。動作するきれいなコードはあらゆる意味で価値がある。

— テスト駆動開発

良いコードかどうかは、変更がどれだけ容易なのかで決まる。

— リファクタリング(第 2 版)

コードは理解しやすくなければいけない。

— リーダブルコード

コードは他の人が最短時間で理解できるように書かなければいけない。

— リーダブルコード

優れたソースコードは「目に優しい」ものでなければいけない。

— リーダブルコード

少なくともテスト駆動開発のゴールに良いコードがあるということはいえるでしょう。え?どうやったら良いコードを書けるようになるかって?私が教えてほしいのですがただ言えることは他の分野と同様に規律の習得と絶え間ない練習と実践の積み重ねのむこうにあるのだろうということだけです。

私がかつて発見した、そして多くの人に気づいてもらいたい効果とは、反復可能な振る舞いを規則にまで還元することで、規則の適用は機会的に反復可能になるということだ。

— テスト駆動開発

ここで、Kent Beck が自ら語ったセリフを思い出しました。「僕は、偉大なプログラマなんかじゃない。偉大な習慣を身につけた少しましなプログラマなんだ」。

— リファクタリング(第 2 版)

参照

参考サイト

参考図書

  • [1] テスト駆動開発 Kent Beck (著), 和田 卓人 (翻訳): オーム社; 新訳版 (2017/10/14)

  • [2] 新装版 リファクタリング―既存のコードを安全に改善する― (OBJECT TECHNOLOGY SERIES) Martin
    Fowler (著), 児玉 公信 (翻訳), 友野 晶夫 (翻訳), 平澤 章 (翻訳), その他: オーム社; 新装版
    (2014/7/26)

  • [3] リファクタリング(第 2 版): 既存のコードを安全に改善する (OBJECT TECHNOLOGY SERIES) Martin
    Fowler (著), 児玉 公信 (翻訳), 友野 晶夫 (翻訳), 平澤 章 (翻訳), その他: オーム社; 第 2 版
    (2019/12/1)

  • [4] リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)
    Dustin Boswell (著), Trevor Foucher (著), 須藤 功平 (解説), 角 征典 (翻訳):
    オライリージャパン; 初版八刷版 (2012/6/23)

  • [5] かんたん Ruby (プログラミングの教科書) すがわらまさのり (著) 技術評論社 (2018/6/21)

  • [6] プロを目指す人のための Ruby 入門 言語仕様からテスト駆動開発・デバッグ技法まで (Software Design plus シリーズ) 伊藤 淳一 (著): 技術評論社 (2017/11/25)

Author: k2works
Link: https://k2works.github.io/2020/04/16/1587009564/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.