私は非常に単純なRuby「FizzBuzz」と呼ばれるゲームの実装を持っています(つまり、入力番号を指定すると、番号が3の倍数の場合は「Fizz」を返し、5の倍数の場合は「Buzz」を返します) 、両方の倍数の場合は「FizzBuzz」、前の条件のいずれにも当てはまらない場合は元の番号):
class FizzBuzz
def answer(number)
multiple3 = number%3 == 0
multiple5 = number%5 == 0
return case
when (multiple3 and multiple5) then "FizzBuzz"
when multiple3 then "Fizz"
when multiple5 then "Buzz"
else number
end
end
end
私はRSpecを使用してテストを作成し、それぞれの条件を検証しました。
require "rspec"
require "./fizzBuzz"
RSpec.describe "#answer" do
it "returns Buzz when number is multiple of 3" do
result = FizzBuzz.new.answer(3)
expect(result).to eq("Fizz")
end
it "returns Buzz when number is multiple of 5" do
result = FizzBuzz.new.answer(5)
expect(result).to eq("Buzz")
end
it "returns a number when the input number is neither multiple of 3 nor 5" do
result = FizzBuzz.new.answer(11)
expect(result).to eq(11)
end
end
テストは完全に機能しますが、具体的な値(3、5、11)を使用しています。
私の質問は、FizzBuzz Rubyスクリプトを広範囲の値(1から10000など)を使用してテストしたい場合はどうなりますか?)
RSpecで各ループとケースを直接使用することでこれを解決できることはわかっていますが、私の懸念は、テストでRubyテストするスクリプトと同じ条件ステートメントを採用する場合(すなわちwhen number%3 == 0 then "Fizz"
など)テストするスクリプトとまったく同じロジックに従うRSpecスクリプトを使用してコードをテストすることになります。したがって、テストはおそらく正常に合格します。
代替案は何でしょうか?ハードコードされた値や特定の値ではなく、幅広い値のプールを使用して(ループを使用するなど)テストを作成するためのベストプラクティスはありますか?
ここで考えられる中間点は、RSpecテストで考えられる答えをループすることです。コードを維持することDRYは重要ですが、テストを維持することも重要ですDRYそしてこれは時々過小評価されます。
このようなものはどうですか:
RSpec.describe "#answer" do
expected_values = {'3': 'Fizz', '5': 'Buzz', '6': 'Fizz', '11': '11', '15': 'FizzBuzz'}
expected_values.each do |val, expected|
it "returns #{expected} when number is #{val}" do
result = FizzBuzz.new.answer(val.to_i)
expect(result).to eq(expected)
end
end
end
そうすれば、テストをexpected_values
ハッシュに追加することで簡単にテストを追加できますが、メソッド名が変更された場合などは、1か所で変更するだけで済みます。
本当に必要な場合に備えて質問しているふりをします。この単純なケースは説明のためだけのものです。
この種の問題をエレガントな方法で解決できるプロパティベースのテストがあります。このアプローチでは、いくつかのプロパティを見つける必要があります(たとえば、2つの数値を追加すると、結果は2つの数値よりも優れ、a + b = b + a ...)。また、特定の場合よりも大きなスペクトルをカバーするために、エントリをランダムに生成するフレームワークを使用します。
Rubyにも役立つフレームワークがあります。
プロパティベースのテストの優れた説明 http://fsharpforfunandprofit.com/posts/property-based-testing/ (免責事項コードはFsharpにあります)
仕様内でサイクルを使用できます。
RSpec.describe "#answer" do
RESULTS = { 3 => "Fizz",
5 => "Buzz",
11 => 11 }
RESULTS.each do |value, answer|
it "returns #{answer} when the input is #{value}" do
result = FizzBuzz.new.answer(value)
expect(result).to eq(answer)
end
end
end
RSpecには、共有例のようなクールな機能が組み込まれています。詳細 こちら
その結果、次のような共有グループが作成されます(このグループは他のテストで使用できることに注意してください)。
shared_examples "shared example" do |number, result|
it "returns #{result} when accepts #{number}" do
# implicit `subject` here is equal to FizzBuzz.new
expect(subject.answer(number)).to eq(result)
end
end
そして、テストはより読みやすく、より「rspecのような」方法で書かれるでしょう:
DATA_SET = {15 => 'FizzBuzz', 3 => 'Fizz', 5 => 'Buzz', 11 => 11}
RSpec.describe FizzBuzz do
context "#answer" do
DATA_SET.each do |number, result|
# pseudo-randomizing test data.
number = [1,2,4,7,8,11,13,14,16].sample*number
result = number if result.is_a?(Integer)
include_examples "shared example", number, result
end
end
end
受け入れられた回答をサポートするために、テストを他のコードから分離したままにするために、より広いテストをコンテキストブロックでラップすることをお勧めします。
RSpec.describe "#answer" do
context 'when testing inputs and answers' do
RESULTS = { 3 => "Fizz",
5 => "Buzz",
11 => 11 }
RESULTS.each do |value, answer|
it "returns #{answer} when the input is #{value}" do
result = FizzBuzz.new.answer(value)
expect(result).to eq(answer)
end
end
end
end