Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

エピソード1

Open in Gitpod

初めに

この記事は一応、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 で保存します。

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

テストを実行します。

$ 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をインストールしておきましょう。

$ 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 コマンドでインストールしたのです。

$ 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!!! に書き換えてテストを実行してみるとどうなるでしょうか。

...
class HelloTest < Minitest::Test
  def test_greeting
    assert_equal 'hello world!!!', greeting
  end
end
...
$ 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を使ったバージョン管理の基本を学んでおきましょう。

$ 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 に書きましょう。最初に何を書くのかって?
アサーションを最初に書きましょう。

アサートファースト

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

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

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

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

— テスト駆動開発

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

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

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

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

— リーダブルコード

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

テストを実行します。

$ 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 クラスメソッド を作成しましょう。ちょっと何言ってるかわからないかもしれませんがとりあえずそんなものだと思って書いてみてください。

...
class FizzBuzz
  def self.generate(n)
    '1'
  end
end

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

$ 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 までの数

  • プリントする

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

...
class FizzBuzz
  def self.generate(n)
    n.to_s
  end
end

テストを実行します。

$ 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で大事なのは、細かいステップを踏むことではなく、細かいステップを踏み続けられるようになることだ。

— テスト駆動開発

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

...
  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
...

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

— リーダブルコード

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

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

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

リファクタリング

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

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

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

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

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

— テスト駆動開発

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

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

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

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

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

メソッドの抽出

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

メソッドの抽出

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

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

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

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 クラス の参照を 代入 して各テストメソッドで共有できるようにしました。ちょっと何言ってるかわからないかもしれませんがここではそんなことをやってるぐらいのイメージで大丈夫です。

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

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

$ 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

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

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

変数名の変更

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

...
class FizzBuzz
  def self.generate(n)
    n.to_s
  end
end

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

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

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

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

— リーダブルコード

...
class FizzBuzz
  def self.generate(number)
    number.to_s
  end
end

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

$ 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

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

...
class FizzBuzz
  def self.generate(number)
    numbr.to_s
  end
end
$ 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リスト の内容に取り掛かるとしましょう。

$ 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 までの数

  • プリントする

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

...
  def test_3を渡したら文字列Fizzを返す
    assert_equal 'Fizz', @fizzbuzz.generate(3)
  end
...
$ 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メソッドくらいシンプルなものを仮実装する必要が本当にあるだろうか。
普通は、その必要はない。頭に浮かんだ明白な実装をただ単にコードに落とすだけだ。もしもレッドバーが出て驚いたら、あらためてもう少し歩幅を小さくしよう。

— テスト駆動開発

class FizzBuzz
  def self.generate(number)
    number.to_s
  end
end

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

class FizzBuzz
  def self.generate(number)
    result = number.to_s
    if number % 3 == 0
       result = 'Fizz'
    end
    result
  end
end
$ 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

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

$ 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 までの数

  • プリントする

class FizzBuzz
  def self.generate(number)
    result = number.to_s
    if number % 3 == 0
       result = 'Fizz'
    end
    result
  end
end

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

class FizzBuzz
  def self.generate(number)
    result = number.to_s
    if number.modulo(3).zero?
       result = 'Fizz'
    end
    result
  end
end

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

アルゴリズムの取り替え

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

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

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

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

— かんたんRuby

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

— かんたんRuby

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

$ 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
$ 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 までの数

  • プリントする

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

...
  def test_5を渡したら文字列Buzzを返す
    assert_equal 'Buzz', @fizzbuzz.generate(5)
  end
end
$ 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式 を使って条件分岐を追加しましょう。

class FizzBuzz
  def self.generate(number)
    result = number.to_s
    if number.modulo(3).zero?
       result = 'Fizz'
    end
    result
  end
end
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
$ 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

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

$ 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 までの数

  • プリントする

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

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

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
$ 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

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

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
$ 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 メソッド呼び出しに失敗したようです。テストメソッドを移動して変数のスコープ範囲に入れましょう。

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

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

$ 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行を目指そう。

— テスト駆動開発

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

— リーダブルコード

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

$ 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 までの数

  • プリントする

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

...
    describe '三と五の倍数の場合' do
      def test_15を渡したら文字列FizzBuzzを返す
        assert_equal 'FizzBuzz', @fizzbuzz.generate(15)
      end
    end
...
$ 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

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

...
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
$ 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をインストールして使うことにしましょう。

$ gem install byebug

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

$ 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メソッドでコード内にブレークポイントを埋め込んでデバッガを実行してみましょう。

...
    describe '三と五の倍数の場合' do
      def test_15を渡したら文字列FizzBuzzを返す
        require 'byebug'
        byebug
        assert_equal 'FizzBuzz', @fizzbuzz.generate(15)
      end
    end
...
$ 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 でもいけます。

(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) メソッド内にステップインします。

(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? の行で判定されるはず・・・

(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'

ファッ!?

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

...
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
$ 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

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

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

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

— テスト駆動開発

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

...
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」とプリントすること。 に沿った記述にするとともにネストした部分をわかりやすくするために アルゴリズムの置き換え を適用してリファクタリングをしましょう。

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

— リーダブルコード

...
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

テストして、

$ 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

コミットです。

$ 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クラスとして定義されているようですね。空の配列を作るには [] (配列リテラル)を使えばいいみたいですね。こんな感じかな?

...
    describe '1から100までの数の配列を返す' do
      def test_配列の初めは文字列の1を返す
        result = []
        assert_equal '1', result
      end
    end
  end
end
$ 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

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

...
    describe '1から100までの数の配列を返す' do
      def test_配列の初めは文字列の1を返す
        result = ['1']
        assert_equal '1', result
      end
    end
  end
end
$ 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

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

...
    describe '1から100までの数の配列を返す' do
      def test_配列の初めは文字列の1を返す
        require 'byebug'
        byebug
        result = ['1']
        assert_equal '1', result
      end
    end
  end
end

デバッガを起動します。

$ 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でブレークポイントまで進めます。

(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 の中身を確認してみましょう。

(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"]

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

(byebug) result
["1"]
(byebug) result[1]
nil

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

(byebug) result
["1"]
(byebug) result[1]
nil
(byebug) result[0]
"1"

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

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

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

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

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

(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"]

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

(byebug) q
Really quit? (y/n) y
...
    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
$ 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 に配列を返すメソッドを作れば良さそうですね。とりあえずメソッド名は今の時点ではあまり考えずに・・・

...
    describe '1から100までの数の配列を返す' do
      def test_配列の初めは文字列の1を返す
        result = FizzBuzz.print_1_to_100
        assert_equal '1', result.first
      end
    end
  end
end
$ 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

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

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
$ 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

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

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

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

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

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

— かんたんRuby

$ 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 メソッドはまだ最後の要素が検証されていませんね。三角測量 を使って小さなステップで進めていくことにしましょう。

...
    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
$ 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入門

$ irb
irb(main):001:0>

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

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

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

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

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まで表示したいのでこうですね。

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 を終了させてプロダクトコードを変更しましょう。

irb(main):011:0> exit
...
  def self.print_1_to_100
    result = []
    (1..100).each do |n|
      result << n
    end
    result
  end
end
$ 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

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

...
  def self.print_1_to_100
    result = []
    (1..100).each do |n|
      result << n.to_s
    end
    result
  end
end
$ 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のでこのように書き換えることができます。

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

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

$ 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

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

$ 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を返す
  • プリントする

...
      def test_配列の2番目は文字列のFizzを返す
        result = FizzBuzz.print_1_to_100
        assert_equal 'Fizz', result[2]
      end
    end
  end
end
$ 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 を呼び出すように変更しましょう。

...
  def self.print_1_to_100
    result = []
    (1..100).each { |n| result << generate(n) }
    result
  end
end
$ 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つ目のテストが失敗しています。これはテストケースが間違っていますね。

...
      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
$ 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

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

...
    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)」は重複しないことが多い。

— テスト駆動開発

...
    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
$ 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

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

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

TODOリスト

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

    • 配列の初めは文字列の1を返す
    • 配列の最後は文字列の100を返す
    • 配列の2番めは文字列のFizzを返す
    • 配列の4番目は文字列のBuzzを返す
    • 配列の14番目は文字列のFizzBuzzを返す
  • プリントする

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

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

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
$ 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
$ git add main.rb
$ git commit -m 'test: 学習用テスト'

コードの不吉な臭い

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

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

— テスト駆動開発

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

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版)

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

— テスト駆動開発

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

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

$ 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 メソッド名の変更したけどテストは以前のままでしたね。

...
    describe '1から100までのFizzBuzzの配列を返す' do
      def setup
        @result = FizzBuzz.print_1_to_100
      end
...
...
    describe '1から100までのFizzBuzzの配列を返す' do
      def setup
        @result = FizzBuzz.generate_list
      end
...
$ 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

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

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

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

— テスト駆動開発

長い関数

長い関数

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

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

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

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

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

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分岐ですがこうします。

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
...
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
...
$ 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文を使ってはいけないと思っている人がいる。アホくさ。関数から早く返すのはいいことだ。むしろ望ましいときもある。

— リーダブルコード

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

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

一時変数のインライン化

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

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

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

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
...
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
...
$ 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

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

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

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

説明用変数の導入

複雑な式がある。

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

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

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
...
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を返す判定部分にこの 説明変数 を適用しました。壊れていないか確認しておきましょう。

説明変数

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

— リーダブルコード

要約変数

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

— リーダブルコード

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
...
$ 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

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

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

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

ループ

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

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

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

...
  def self.generate_list
    result = []

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

    result
  end
end

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

...
  def self.generate_list
    result = []

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

    result = []
    result
  end
end
$ 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版)

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

— リーダブルコード

...
  def self.generate_list
    return (1..100).each { |n| result << generate(n) }
  end
end
$ 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 は使わないので

...
  def self.generate_list
    return (1..100).each { |n| generate(n) }
  end
end
$ 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

...
  def self.generate_list
    return (1..100).map { |n| generate(n) }
  end
end
 $ 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を省略できるので

...
  def self.generate_list
    (1..100).map { |n| generate(n) }
  end
end
$ 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入門

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

マジックナンバー

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

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

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

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

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

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

class FizzBuzz
  MAX_NUMBER = 100

...

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

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

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

— かんたんRuby

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

— リーダブルコード

$ 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の 単一行コメントアウト のやり方は行頭に # を使います。

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

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

コメント

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

— リファクタリング(第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

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

$ 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クラスを読み込めるようにする必要があります。

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

$ 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
require './lib/fizz_buzz.rb'

puts FizzBuzz.generate_list

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

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

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

$ ruby main.rb
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
...
Buzz

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

$ 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"] $

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

$ 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.

require './lib/fizz_buzz.rb'

puts FizzBuzz.generate_list

/lib/fizz_buzz.rb.

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.

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は分析技法であり、設計技法であり、実際には開発のすべてのアクティビティを構造化する技法なのだ。

— テスト駆動開発

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

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
$ 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版)

エピソード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)

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away