Last active
June 13, 2018 11:30
-
-
Save lukeredpath/6a29b864d10b7141813729f8f569771e to your computer and use it in GitHub Desktop.
FizzBuzz with mocks
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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