Skip to content

Instantly share code, notes, and snippets.

@lukeredpath
Last active June 13, 2018 11:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lukeredpath/6a29b864d10b7141813729f8f569771e to your computer and use it in GitHub Desktop.
Save lukeredpath/6a29b864d10b7141813729f8f569771e to your computer and use it in GitHub Desktop.
FizzBuzz with mocks
# Given a range as input, reports fizz, buzz or fizzbuz.
#
# FizzBuzz logic is separated from presentation logic which is a concern for the reporter.
#
class FizzBuzz
def generate(range, reporter)
range.each do |n|
if n % 3 == 0 && n % 5 == 0
reporter.report(:fizzbuzz)
elsif n % 3 == 0
reporter.report(:fizz)
elsif n % 5 == 0
reporter.report(:buzz)
else
reporter.report(n)
end
end
end
end
# The accompanying test uses a mock to define the contract with the 'reporter' collaborator.
# The reporter becomes a distinct role but we aren't concerned with the details of what the reporter does, only how FizzBuzz interacts with it.
# The reporter could be injected into FizzBuzz in the initializer or the generate method depending on how we want to use FizzBuzz.
describe "FizzBuzz" do
subject { FizzBuzz.new }
let(:reporter) { double('reporter') }
it "reports :fizz for multiples of 3" do
expect(reporter).to receive(:report).with(:fizz)
subject.generate(3..3, reporter)
end
it "reports :buzz for multiples of 5" do
expect(reporter).to receive(:report).with(:buzz)
subject.generate(3..3, reporter)
end
it "reports :fizzbuzz for multiples of 3 and 5" do
expect(reporter).to receive(:report).with(:fizzbuzz)
subject.generate(15..15, reporter)
end
it "reports the number for anything else" do
expect(reporter).to receive(:report).with(1)
subject.generate(1, reporter)
end
end
# "Reporter" now has a well defined contract - it has a #report method that takes one of three symbols or an Int. It is a "port".
# We'll need a concrete implementation. For now we're happy to print it to STDOUT but we're happy to leap ahead a bit and allow for any IO stream.
# This is our adapter to the outside world.
class IOFizzBuzzReporter
def initialize(stream = STDOUT)
@stream = stream
end
def report(value)
case value
when :fizz
@stream << "Fizz"
when :buzz
@stream << "Buzz"
when :fizzbuzz
@stream << "FizzBuzz"
else
@stream << value.to_s
end
end
end
# How would we test this?
# We could write an interaction based test, but this would be an example of *bad* mocking:
describe IOFizzBuzzReporter
it "can handle :fizz" do
stream = double('stream')
reporter = IOFizzBuzzReporter.new(stream)
expect(reporter).to receive(:<<).with("Fizz")
reporter.report(:fizz)
end
end
# The above test violates the "only mock types you own" rule.
# Precisely how we interact with the IO object is not the important behaviour here.
# The important behaviour is that we write the correct value.
# This test would break if we changed the implementation to call :puts instead of :<< even though we haven't changed the behaviour.
# As this is an adapter, a state based test with a simple fake that acts like any IO object might be better.
# If it's hard to fake, we might choose an integration test instead.
describe IOFizzBuzzReporter
it "can handle :fizz" do
stream = StringIO.new
reporter = IOFizzBuzzReporter.new(stream)
reporter.report(:fizz)
stream.rewind
expect(stream).to contain("Fizz")
end
end
# This test now better represents the behaviour being tested and we are free to refactor.
# In the first test, the call to our mock *was* the behaviour under test. In the above, we are more interested in changes to the outside world (the IO).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment