LoginSignup
12

More than 5 years have passed since last update.

抽象的なテストのススメ(FizzBuzzの例で)

Posted at

Ruby Advent Calender 2014の19日目です。

FizzBuzzとは以下の仕様を満たすプログラムを作る有名な問題です。

  • 3の倍数かつ5の倍数の場合には"FizzBuzz"を出力する
  • 3の倍数かつ5の倍数でない場合には"Fizz"を出力する
  • 3の倍数でなくかつ5の倍数の場合には"Buzz"を出力する
  • 3の倍数でも5の倍数でもない場合には入力された数字そのものを返す

実装してみるとこんな感じになります。

def fizzbuzz x
  return "FizzBuzz" if x % 15 == 0
  return "Fizz" if x % 3 == 0
  return "Buzz" if x % 5 == 0
  return x
end

本記事の主張は「FizzBuzzぐらいは仕様通りに確実にテストしたいよね」ってことです。
非常に巷に転がっているテストコードにもやもやしています。
FizzBuzzに対するテストコードのよくある書き方と理想的な書き方について試行錯誤してみます。

よくあるテストの書き方

describe :main do
  it {expect(fizzbuzz 15).to  eq "FizzBuzz"}
  it {expect(fizzbuzz 3).to   eq "Fizz"}
  it {expect(fizzbuzz 5).to   eq "Buzz"}
  it {expect(fizzbuzz 11).to  eq 11}
end

どの言語でテストを書いても、ほとんどはこのように実際に値を入力してテストする方法になるかと思います。
この方法だと以下の問題があります。

  • 特定の場合のチェックのみで、それ以外の場合は保証しない
  • テストが断片的で、テストから仕様が読み取れない

自分が考える理想形とGem abst_int

この問題を解決するために、abst_intというGemを作りました。
このGemは、「3の倍数」といったオブジェクトを作ることができます。
例えば、こんな感じです。

abst2 = (AbstInt.new * 2).object # 2の倍数
abst3 = (AbstInt.new * 3).object # 3の倍数
abst2 % 2 #=> 0
abst3 % 3 #=> 0
abst2 % 3 #=> AbstInt::MultiResultError
(abst2 + 1) % 2 #=> 1

abst2_and_3 = ((AbstInt.new * 2) & (AbstInt.new * 3)).object  # 2の倍数かつ3の倍数
abst2_and_3 % 6 #=> 0
abst2_and_3 % 2 #=> 0
abst2_and_3 % 3 #=> 0

not_abst3 = (AbstInt.new * 3).not.object # 3の倍数でない
not_abst3 % 3 #=> 1 or 2 を表現するオブジェクト
not_abst3 % 3 == 0 #=> false
not_abst3 % 3 == 1 #=> AbstInt::MultiResultError

(AbstInt.new * 2).objectにて「2の倍数」を作ることができます。
2などの具体的な値は裏では持たず、「2の倍数」共通の動作を行うようにしています。
「2の倍数」に2を足しても「2の倍数」であり続けるなどはmockなどで組むには大変だと思います。

このGemを使うとFizzBuzzのテストは以下のように書けます。

describe :main do
  let(:x35) { (AbstInt.new * 3       & AbstInt.new * 5).object }
  let(:x3)  { (AbstInt.new * 3       & (AbstInt.new * 5).not).object }
  let(:x5)  { ((AbstInt.new * 3).not & AbstInt.new * 5).object }
  let(:x)   { ((AbstInt.new * 3).not & (AbstInt.new * 5).not).object }

  it {expect(fizzbuzz x35).to eq "FizzBuzz"}
  it {expect(fizzbuzz x3).to  eq "Fizz"}
  it {expect(fizzbuzz x5).to  eq "Buzz"}
  it {expect(fizzbuzz x).to   eq x}
end

テストコードも仕様を読み取りやすく書けるようになったかなと思います。

まとめ

Rubyだからこそではありますが、一味違ったテスト方法を紹介しました。
「バグのないシステムはない」とは言いますが、少しでも正確なシステムを作りたいものです。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12