21

I have a very simple Ruby implementation of a game called "FizzBuzz" (i.e. given an input number, it returns "Fizz" if the number is multiple of 3, "Buzz" if multiple of 5, "FizzBuzz" if multiple of both and the original number if it doesn't fit any of the previous condition):

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

I wrote a test using RSpec to validate each one of the conditions:

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

The test works perfectly, however, I'm using concrete values in it (i.e. 3, 5 and 11).

My question is: what if I wanted to test my FizzBuzz Ruby script using a wide range of values (e.g. from 1 to 10000)?

I know I can solve this by using each loops and cases directly in RSpec, however, my concern is that if in my test I adopt the same conditional statements as in the Ruby script to be tested (i.e. when number%3 == 0 then "Fizz", etc.) I will end up testing my code using an RSpec script that follows exactly the same logic as the script to be tested, hence the test will probably pass successfully.

What would be the alternative? Are there best practices to write tests using a wide pool of values (e.g. using a loop) rather than hard-coded or specific values?

Albz
  • 1,982
  • 2
  • 21
  • 33
  • 2
    The whole point of unit testing is to check the code against known results for known inputs. If you test your code using other code then there is a good chance that you will have a bug in both of them and the test will pass or fail when it should not. – Mike Szyndel Jan 18 '16 at 14:52
  • Your code has only 4 possible outcomes. Why do you think there is a benefit in testing 10000 iterations? – spickermann Jan 18 '16 at 14:52

5 Answers5

37

A possible half-way point here is to loop through possible answers in your RSpec tests. Keeping your code DRY is important but so is keeping your tests DRY and this is sometimes under-estimated.

How about something like this:

  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

That way, you can add tests easily by adding them to the expected_values hash, but if the method name changed or something similar you would only have to change it in one place

Yule
  • 9,668
  • 3
  • 51
  • 72
  • Would an each loop work around the describe block? Leaving aside whether that would ever be a good idea... – Jay May 05 '16 at 08:23
  • 1
    Yes, you could put a loop around the describe block. It may be a good idea if you have the right scenario for it i.e. a bunch of tests that need to work for certain inputs – Yule May 05 '16 at 10:14
  • Absolutely Brilliant! – mrateb Nov 01 '22 at 13:37
8

In support of the accepted answer I would suggest wrapping the wider test in a context block, so that the test remains isolated from other code:

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
eazy_beans
  • 512
  • 4
  • 5
3

RSpec now has built-in cool features like shared examples. More info here.

As a result we'll get shared group like this (note, that this group can be used in other tests):

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

And tests will be more readable and written in more "rspec-like" way:

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
Sergii K
  • 845
  • 9
  • 16
1

I will pretend that you are asking in case you will really need it and this simple case is just for illustration.

There is Property Based Testing who can resolve this kind of issue in an elegant manner, with this approach you have to find some property (for example in the addition of the two number the result is superior to the two number and a + b = b + a...). And you will use a framework who will generate randomly the entries in order to cover a larger spectra than if it was a specific cases.

There is framework who can helps in Ruby as well.

An excellent explanation of Property Based Testing http://fsharpforfunandprofit.com/posts/property-based-testing/ (disclaimer the code is in Fsharp)

rad
  • 1,857
  • 1
  • 20
  • 30
1

You can use cycles inside your specs:

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
bukk530
  • 1,858
  • 2
  • 20
  • 30