nigoblog

技術系会社のCEOブログ~私的編~

テスト駆動開発でFizzBuzz問題を解く

お久しぶりの更新です。
今回はFizzBuzz問題テスト駆動開発(以下TDD)で解く手順について書いていきます。
使用言語はrubyです。

  1. FizzBuzz問題とは?
  2. テストコード
  3. 実装
  4. まとめ

以上のような流れで書いていきます。

FizzBuzz問題とは?

早速例を以下に示します

1, 2, fizz, 4, buzz, fizz, 7, 8 fizz, buzz, 11, fizz, 13, 14, fizzbuzz

このように1から数えていき、3の倍数であればfizz、5の倍数であればbuzz、どちらも満たしていればfizzbuzzを返すという問題です。

テストコード

ではfizzbuzzの実装に入っていくのですが、TDDではまずはテストコードを書きます。テストコードは次のようになります。

# -*- coding: utf-8 -*-
require 'test/unit'
require './fizz_buzz'

class FizzBuzzTest < Test::Unit::TestCase
  def setup
    @fizz_buzz = FizzBuzz.new
  end

  def test_at1
    r = @fizz_buzz.at 1
    assert_equal(1, r)
  end

  def test_at3
    r = @fizz_buzz.at 3
    assert_equal('fizz', r)
  end

  def test_at5
    r = @fizz_buzz.at 5
    assert_equal('buzz', r)
  end

  def test_at15
    r = @fizz_buzz.at 15
    assert_equal('fizzbuzz', r)
  end

  def teardown
  end
end

上から説明すると

  1. test/unit、fizz_buzzファイルの読み込み
  2. setupでFizzBuzzインスタンスを作成
  3. testメソッドでは1, 3, 5, 15の時に結果がどうなるかをチェック
  4. テストコードはsetup~teardownの間に書く

最初はこれをコピペでも良いかと思います。
そして実行すると次のようになります。

/usr/local/rvm/rubies/ruby-1.9.3-p194/lib/ruby/site_ruby/1.9.1/rubygems/custom_require.rb:36:in `require': cannot load such file -- ./fizz_buzz (LoadError)
	from /usr/local/rvm/rubies/ruby-1.9.3-p194/lib/ruby/site_ruby/1.9.1/rubygems/custom_require.rb:36:in `require'
	from fizz_buzz_test.rb:3:in `<main>'

fizz_buzzファイルがないということです。
というわけで作って行きましょう!!

実装

まずはfizz_buzzファイルを作成します。
fizz_buzz.rb

class FizzBuzz
end

そこでテストコードを実行すると次のようになります。

# Running tests:

EEEE

Finished tests in 0.002399s, 1667.3614 tests/s, 0.0000 assertions/s.

  1) Error:
test_at1(FizzBuzzTest):
NoMethodError: undefined method `at' for #<FizzBuzz:0x007fb5c1982ed8>
    fizz_buzz_test.rb:11:in `test_at1'

  2) Error:
test_at15(FizzBuzzTest):
NoMethodError: undefined method `at' for #<FizzBuzz:0x007fb5c1982438>
    fizz_buzz_test.rb:26:in `test_at15'

  3) Error:
test_at3(FizzBuzzTest):
NoMethodError: undefined method `at' for #<FizzBuzz:0x007fb5c19819c0>
    fizz_buzz_test.rb:16:in `test_at3'

  4) Error:
test_at5(FizzBuzzTest):
NoMethodError: undefined method `at' for #<FizzBuzz:0x007fb5c183f800>
    fizz_buzz_test.rb:21:in `test_at5'

4 tests, 0 assertions, 0 failures, 4 errors, 0 skips

ざっくりいうとメソッドがないと言われています。
なのでメソッドを作ります。

class FizzBuzz
  def at
  end
end

すると

# Running tests:

EEEE

Finished tests in 0.002626s, 1523.2292 tests/s, 0.0000 assertions/s.

  1) Error:
test_at1(FizzBuzzTest):
ArgumentError: wrong number of arguments (1 for 0)
    /Users/nigorinumahiroki/prog/ruby/FizzBuzzTestCode/fizz_buzz.rb:2:in `at'
    fizz_buzz_test.rb:11:in `test_at1'

  2) Error:
test_at15(FizzBuzzTest):
ArgumentError: wrong number of arguments (1 for 0)
    /Users/nigorinumahiroki/prog/ruby/FizzBuzzTestCode/fizz_buzz.rb:2:in `at'
    fizz_buzz_test.rb:26:in `test_at15'

  3) Error:
test_at3(FizzBuzzTest):
ArgumentError: wrong number of arguments (1 for 0)
    /Users/nigorinumahiroki/prog/ruby/FizzBuzzTestCode/fizz_buzz.rb:2:in `at'
    fizz_buzz_test.rb:16:in `test_at3'

  4) Error:
test_at5(FizzBuzzTest):
ArgumentError: wrong number of arguments (1 for 0)
    /Users/nigorinumahiroki/prog/ruby/FizzBuzzTestCode/fizz_buzz.rb:2:in `at'
    fizz_buzz_test.rb:21:in `test_at5'

4 tests, 0 assertions, 0 failures, 4 errors, 0 skips

今度はメソッドにいれる引数が変だよと言われます

なので

class FizzBuzz
  def at(n)
  end
end

結果は

# Running tests:

FFFF

Finished tests in 0.004781s, 836.6451 tests/s, 836.6451 assertions/s.

  1) Failure:
test_at1(FizzBuzzTest) [fizz_buzz_test.rb:12]:
<1> expected but was
<nil>.

  2) Failure:
test_at15(FizzBuzzTest) [fizz_buzz_test.rb:27]:
<"fizzbuzz"> expected but was
<nil>.

  3) Failure:
test_at3(FizzBuzzTest) [fizz_buzz_test.rb:17]:
<"fizz"> expected but was
<nil>.

  4) Failure:
test_at5(FizzBuzzTest) [fizz_buzz_test.rb:22]:
<"buzz"> expected but was
<nil>.

4 tests, 4 assertions, 4 failures, 0 errors, 0 skips

若干進みました!
というわけでテスト1を成功させてみます。

class FizzBuzz
  def at(n)
    return 1
  end
end

すると

# Running tests:

.FFF

Finished tests in 0.004811s, 831.4280 tests/s, 831.4280 assertions/s.

  1) Failure:
test_at15(FizzBuzzTest) [fizz_buzz_test.rb:27]:
<"fizzbuzz"> expected but was
<1>.

  2) Failure:
test_at3(FizzBuzzTest) [fizz_buzz_test.rb:17]:
<"fizz"> expected but was
<1>.

  3) Failure:
test_at5(FizzBuzzTest) [fizz_buzz_test.rb:22]:
<"buzz"> expected but was
<1>.

4 tests, 4 assertions, 3 failures, 0 errors, 0 skips

一番上の . FFFに注目すると先ほどまではFFFFだったのに対し今度は . FFFになっています。
これは最初のアサーションが成功しているということです。
次は一気に全部成功させてみましょう。

class FizzBuzz
  def at(n)
    if n == 1
      return 1
    elsif n==3
      return 'fizz'
    elsif n==5
      return 'buzz'
    else
      return 'fizzbuzz'
    end
  end
end

結果は

# Running tests:

....

Finished tests in 0.002073s, 1929.5707 tests/s, 1929.5707 assertions/s.

4 tests, 4 assertions, 0 failures, 0 errors, 0 skips

全部成功!!
完成!!

。。。ではないことは明らかですね。というわけでテストコードを若干変更

def test_at6
  r = @fizz_buzz.at 6
  assert_equal('fizz', r)
end

上記のコードを追加します。
すると確実にこれはfailureになります。
というわけでハードコーディングでは限界があるので上記のテストも合格するコードを書きます

class FizzBuzz
  def at(n)
    fizz = false
    buzz = false
    if (n%3) == 0
      fizz = true
    end
    if (n%5) == 0
      buzz = true
    end
    
    if fizz && buzz
      return 'fizzbuzz'
    elsif fizz && !buzz
      return 'fizz'
    elsif buzz && !fizz
      return 'buzz'
    else
      return n
    end
  end
end

上記のコードをテストすると。。。

# Running tests:

....

Finished tests in 0.001967s, 2033.5536 tests/s, 2033.5536 assertions/s.

4 tests, 4 assertions, 0 failures, 0 errors, 0 skips

というわけで合格しました!!
これで本当に完成です。

ただし万全ではないです。仮に 0以下の値が入ったら。。。などというのは考えていません。
最低限のfizzbuzzです。
ちなみに実行ファイルも作ってみました。
fizz_buzz_do.rb

require './fizz_buzz.rb'

@fizz_buzz = FizzBuzz.new

for i in 1..15
  p @fizz_buzz.at i
end

結果は

1
2
"fizz"
4
"buzz"
"fizz"
7
8
"fizz"
"buzz"
11
"fizz"
13
14
"fizzbuzz"

となります。

まとめ

以前もTDDの記事を書きました。
今回はそのためだいぶ要領よくできました。
ちなみに今回のコードはgithubにも上がっているのでチェケラーよろしくお願いします。
nigohiroki/FizzBuzzTestCode · GitHub

個人的にTDDのどんどん出来上がっていく感じが好きです。
rubyはデフォルトでtestunitが入っているのでとっつきやすいと思います。

そんなわけで「もっとよいコードをかけるぜ!!」
などという人がいればどんどんコメントお願いします!!
それでは~