プログラミングを頑張る日記

プログラミングを勉強して、ハッカーになろう

Rubyを頑張る(1)

さて、いよいよRubyの勉強に着手したいと思います。
少しでもEmacsで快適な環境にしていこうと思い、とりあえず下記設定を追加しました。

;;; smart-compileを導入
(require 'smart-compile)
(define-key ruby-mode-map (kbd "C-c c") 'smart-compile)
(define-key ruby-mode-map (kbd "C-c C-c") (kbd "C-c c C-m"))

C-c C-cで編集中のファイルを実行して、その結果を表示してくれるらしいです。

また、Rubyの勉強のために、「プロを目指す人のためのRuby入門」を購入しました。
あんまり最初からやっても(自分の)心が折れるので、3章からやりたいと思います。

3章は「テストを自動化する」というテーマであり、自分はテストをちゃんとやったことがありません。
いや、やったことはありますが、それは画面をポチポチしてスクショし、excelに貼りつけるようなものであり、
自動テストというものに憧れがあるのです。
それではやっていきます。

整数を与えるとFizzBuzzにのっとったルールで値を返す関数を作り、それを自動テストする内容のようです。
Minitestというテスト用フレームワークを使うようですが、なぞるだけではつまらないので、
RSpecというのを使ってみたいと思います。

RSpec事始め

まずは準備です。

gem install rspec

コマンドラインで上記を実行しました。
なにやらどかどかと出力があり、

6 gems installed

終わったようです。

さて、RSpecを使うためには設定ファイルが必要らしく、それはこのように作るそう。

MB-Air:ruby User$ rspec --init
  create   .rspec
  create   spec/spec_helper.rb
MB-Air:ruby User$ ls -a
.	..	.rspec	spec	study	test
MB-Air:ruby User$

testというディレクトリも用意していたのですが、無駄だったようです。
spec/spec_helper.rbというのは便利な設定ができるファイルだそうです。
まだお世話にならなさそう。
.rspecというファイルは、rspecコマンドにデフォルトで渡すコマンドオプションだそう。
何かオプションを追加したいときは.rspecに書くわけですね。

テストを書くファイルは慣習として、specディレクトリの中に書くようです。
大体これで準備完了でしょうか。
しかし新しいことを始めるとわくわくします。

そもそもテスト駆動開発とは

簡単に言うと、
1.これから実装する機能のテストを書き、実行。通らないことを確認。
2.テストが通るように実装する。
3.リファクタリングする。

1の時点で、仕様をしっかりと理解し、テストを書くことが大事ですね。
仕事でこれをするのは大変だろうなあ。しかし自動化されたテストは残るし、手戻りも少なくなるのかな。
なにはともあれ、やっていきます。

テスト駆動開発開始

まずは仕様を理解ですね。
3の倍数が渡されたらFizzを返し、5の倍数が渡されたらBuzzを返す。15の倍数なら、FizzBuzzです。
そしてそれ以外の数が渡されたら、その整数をそのまま戻り値とします。

テストとしては、3ならFizz、5ならBuzz、15ならFizzBuzz、適当に14なら14が返る、みたいなのが最低ラインでしょうか。
さて、テストの書き方もわかっていませんが、とりあえずファイルを作ります。

MB-Air:ruby User$ cd spec
MB-Air:spec User$ touch fizzbuzz_spec.rb
MB-Air:spec User$

あ、ファイル名も慣習として、「テストしたいクラス名_spec.rb」とするらしいです。

使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」
書き方はここを参考にしていきます。

describe 'FizzBuzz' do
  it '3で割り切れる場合' do
    expect(fizz_buzz(3)).to eq('Fizz')
  end
  it '5で割り切れる場合' do
    expect(fizz_buzz(5)).to eq('Buzz')
  end
  it '15で割り切れる場合' do
    expect(fizz_buzz(3)).to eq('FizzBuzz')
  end
  it '3,5,15で割り切れない場合' do
    expect(fizz_buzz(14)).to eq(14)
  end
end

...これでいいのかな。
rspecで実行できるらしいので実行します。

MB-Air:spec User$ rspec
No examples found.


Finished in 0.00085 seconds (files took 0.34317 seconds to load)
0 examples, 0 failures

MB-Air:spec User$

認識されていないようです。ふむむ。

MB-Air:spec User$ cd ..
MB-Air:ruby User$ rspec
FFFF

Failures:

  1) FizzBuzz 3で割り切れる場合
     Failure/Error: expect(fizz_buzz(3)).to eq('Fizz')

     NoMethodError:
       undefined method `fizz_buzz' for #<RSpec::ExampleGroups::FizzBuzz:0x00007fe6f0a357f8>
     # ./spec/fizzbuzz_spec.rb:4:in `block (2 levels) in <top (required)>'

  2) FizzBuzz 5で割り切れる場合
     Failure/Error: expect(fizz_buzz(5)).to eq('Buzz')

     NoMethodError:
       undefined method `fizz_buzz' for #<RSpec::ExampleGroups::FizzBuzz:0x00007fe6f200a0b0>
     # ./spec/fizzbuzz_spec.rb:7:in `block (2 levels) in <top (required)>'

  3) FizzBuzz 15で割り切れる場合
     Failure/Error: expect(fizz_buzz(3)).to eq('FizzBuzz')

     NoMethodError:
       undefined method `fizz_buzz' for #<RSpec::ExampleGroups::FizzBuzz:0x00007fe6f09f0798>
     # ./spec/fizzbuzz_spec.rb:10:in `block (2 levels) in <top (required)>'

  4) FizzBuzz 3,5,15で割り切れない場合
     Failure/Error: expect(fizz_buzz(14)).to eq(14)

     NoMethodError:
       undefined method `fizz_buzz' for #<RSpec::ExampleGroups::FizzBuzz:0x00007fe6f0992008>
     # ./spec/fizzbuzz_spec.rb:13:in `block (2 levels) in <top (required)>'

Finished in 0.01953 seconds (files took 0.67278 seconds to load)
4 examples, 4 failures

Failed examples:

rspec ./spec/fizzbuzz_spec.rb:3 # FizzBuzz 3で割り切れる場合
rspec ./spec/fizzbuzz_spec.rb:6 # FizzBuzz 5で割り切れる場合
rspec ./spec/fizzbuzz_spec.rb:9 # FizzBuzz 15で割り切れる場合
rspec ./spec/fizzbuzz_spec.rb:12 # FizzBuzz 3,5,15で割り切れない場合

MB-Air:ruby User$

なんだかわかりませんが、一個上のディレクトリに移動してもう一度実行したら失敗してくれました。
メソッドが無いそうです。ふっふっふ、そうでしょうとも。

実装

さて! 実装するとしましょう。
"~/Desktop/ruby/study"の下に、fizz_buzz.rbというファイルを作り、そこに実装します。
でも実装する前に、本当にテストをパスできるのか、仮のコードを書きます。

def fizz_buzz (n)
  if n == 3
    'Fizz'
  elsif n == 5
    'Buzz'
  elsif n == 15
    'FizzBuzz'
  else
    14
  end
end

テストに通ることだけを考えた関数です。

で、fizzbuzz_spec.rbに下記を足せばいいのかな。

require '../study/fizz_buzz'

ではやってみます。

MB-Air:ruby User$ rspec

An error occurred while loading ./spec/fizzbuzz_spec.rb.
Failure/Error: require '../study/fizz_buzz'

LoadError:
  cannot load such file -- ../study/fizz_buzz
# ./spec/fizzbuzz_spec.rb:2:in `<top (required)>'
No examples found.


Finished in 0.00081 seconds (files took 0.6519 seconds to load)
0 examples, 0 failures, 1 error occurred outside of examples

MB-Air:ruby User$

むむむ。読み込めないと。
...相対パスを読み込むときは、require_relativeなのですね。
これが問題の本質なのかはわかりませんが、とりあえず直します。

require_relative '../study/fizz_buzz'

さて、テストを実行。

B-Air:ruby User$ rspec
..F.

Failures:

  1) FizzBuzz 15で割り切れる場合
     Failure/Error: expect(fizz_buzz(3)).to eq('FizzBuzz')

       expected: "FizzBuzz"
            got: "Fizz"

       (compared using ==)
     # ./spec/fizzbuzz_spec.rb:11:in `block (2 levels) in <top (required)>'

Finished in 0.10972 seconds (files took 0.60038 seconds to load)
4 examples, 1 failure

Failed examples:

rspec ./spec/fizzbuzz_spec.rb:10 # FizzBuzz 15で割り切れる場合

MB-Air:ruby User$

なんと15で割り切れる場合の引数を間違っていました。
しかし、ちゃんと想定通りのテストが行なわれていそうです。
怪我の功名。

そんじゃちゃんと実装します。

def fizz_buzz (n)
  if n % 15 == 0
    'FizzBuzz'
  elsif n % 3 == 0
    'Fizz'
  elsif n % 5 == 0
    'Buzz'
  else
    n.to_s
  end
end

うん、これでいいはずです。
それではテストコードを直し、本テストをします。

# coding: utf-8
require_relative '../study/fizz_buzz'
describe 'FizzBuzz' do
  it '3で割り切れる場合' do
    expect(fizz_buzz(3)).to eq('Fizz')
  end
  it '5で割り切れる場合' do
    expect(fizz_buzz(5)).to eq('Buzz')
  end
  it '15で割り切れる場合' do
    expect(fizz_buzz(15)).to eq('FizzBuzz')
  end
  it '3,5,15で割り切れない場合' do
    expect(fizz_buzz(14)).to eq(14)
  end
end
MB-Air:ruby User$ rspec
....

Finished in 0.02108 seconds (files took 0.59943 seconds to load)
4 examples, 0 failures

MB-Air:ruby User$

ちゃんと通りました! これがテスト駆動開発か。
ここからリファクタリングですが、自分のRuby力(ぢから)では、これ以上弄れません...。
きっともっとスマートな書き方があるといつも思うのですが。
でも一番FizzBuzzだとわかりやすくていいとも思います。

さて、これからはRubyもびしばし書いていきます。よろしくお願いします!