初めに この記事は一応、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 リスト
まず 数を文字列にして返す
作業に取り掛かりたいのですがまだプログラミング対象としてはサイズが大きいようですね。もう少し具体的に分割しましょう。
これならプログラムの対象として実装できそうですね。
テストファーストから始めるテスト駆動開発 テストファースト 最初にプログラムする対象を決めたので早速プロダクトコードを実装・・・ではなく テストファースト で作業を進めていきましょう。まずはプログラムを実行するための準備作業を進める必要がありますね。
テストファースト
いつテストを書くべきだろうか—-それはテスト対象のコードを書く前だ。
— テスト駆動開発
では、どうやってテストすればいいでしょうか?テスティングフレームワークを使って自動テストを書きましょう。
テスト(名詞) どうやってソフトウェアをテストすればよいだろか—-自動テストを書こう。
— テスト駆動開発
今回 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 world
を hello 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" , test_greeting 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”を返す プログラムを 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を返す" , test_1を渡したら文字列1を返す 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 を返すようにできました。では、2 を渡したらどうなるでしょうか?
TODO リスト
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を返す" , test_2を渡したら文字列2を返す 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 リスト
たかが 数を文字列にして返す プログラムを書くのにこんなに細かいステップを踏んでいくの?と思ったかもしれません。プログラムを書くということは細かいステップを踏んで行くことなのです。そして、細かいステップを踏み続けることが大切なことなのです。
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つ書く。
グリーン:そのテストを迅速に動作させる。このステップでは罪を犯してもよい。
リファクタリング:テストを通すために発生した重複をすべて除去する。
レッド・グリーン・リファクタリング。それが 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を返す" , test_1を渡したら文字列1を返す 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 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を返す" , test_3を渡したら文字列Fizzを返す --- expected +++ actual @@ -1 +1,3 @@ -"Fizz" + + +"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 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 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を返す" , test_5を渡したら文字列Buzzを返す --- expected +++ actual @@ -1 +1,2 @@ -"Buzz" + +"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 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を返す" , test_1を渡したら文字列1を返す NoMethodError: NoMethodError: undefined method `generate' for nil:NilClass main.rb:22:in `test_1を渡したら文字列1を返す' ERROR["test_3を渡したら文字列Fizzを返す" , test_3を渡したら文字列Fizzを返す NoMethodError: NoMethodError: undefined method `generate' for nil:NilClass main.rb:30:in `test_3を渡したら文字列Fizzを返す' ERROR["test_5を渡したら文字列Buzzを返す" , test_5を渡したら文字列Buzzを返す NoMethodError: NoMethodError: undefined method `generate' for nil:NilClass main.rb:34:in `test_5を渡したら文字列Buzzを返す' ERROR["test_2を渡したら文字列2を返す" , test_2を渡したら文字列2を返す 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 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を返す" , test_15を渡したら文字列FizzBuzzを返す 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を返す" , 0529224] test_15を渡したら文字列FizzBuzzを返す 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 をインストールして使うことにしましょう。
インストールが完了したら早速 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'
引数の number
は 15
だから 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 リスト
数を引数にして文字列を返す 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を返す" , 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を返す" , test_配列の初めは文字列の1を返す 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を返す" , 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を返す" , 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を返す" , test_配列の最後は文字列の100を返す 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(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 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を返す" , 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 .0160 8900000428548] test_ 配列の2 番目は文字列のをFizz返す --- expected +++ actual @@ -1 +1 ,3 @@ -"Fizz" + + +"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 .001347000004 898291] test_ 配列の最後は文字列の100 を返す 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 リスト
配列や繰り返し処理の理解 まだリファクタリングが残っているのですがその前に 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を返す" , 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返す" , test_配列の14番目は文字列のをFizzBuzz返す 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返す" , test_配列の4番目は文字列のをBuzz返す 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返す" , test_配列の4番目は文字列のをBuzz返す 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 ..MAX_NUMBER).map { |n| generate(n) } end end
ここではなぜこのような処理を選択したかをコメントしましたが何でもコメントすればよいというわけではありません。
コメント
ここでコメントについて言及しているのは、コメントが消臭剤として使われることがあるからです。コメントが非常に丁寧に書かれているのは、実はわかりにくいコードを補うためだったとうことがよくあるのです。
— リファクタリング(第 2 版)
コメントを書くのであれば、正確に書くべきだ(できるだけ明確で詳細に)。また、コメントには画面の領域を取られるし、読むのにも時間がかかるので、簡潔なものでなければいけない。
— リーダブルコード
1 2 $ git add main.rb $ git commit -m 'refactor: マジックナンバーの置き換え'
動作するきれいなコード TODO リスト
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を返す" , test_1を渡したら文字列1を返す 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 リスト
ふりかえり 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 ..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つずつ書いて通していくことではありません。
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 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 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)