Skip to content

Instantly share code, notes, and snippets.

@subelsky
Created August 30, 2016 14:14
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 subelsky/bf1a02690576ce1f2dfa68fba0d15656 to your computer and use it in GitHub Desktop.
Save subelsky/bf1a02690576ce1f2dfa68fba0d15656 to your computer and use it in GitHub Desktop.
RSpec Template explaining basic test setup
# There should usually be one and only one require statement at the top of a spec, explicitly loading
# the only class this test relates to. That file should include any additional dependencies needed
# by the class (activesupport, nokogiri, etc.)
require "widget"
# Let's pretend we are testing a class called Widget that has only one method, .call (and in fact,
# almost all of our classes should be so simple they just have one method).
#
# class Widget
# # If you allow dependencies to be injected into your class, it's much easier to test and
# # reduces your test's knowledge of what other things are called. We can more easily change
# # the name of ImportantDependency later
# attr_writer :important_dependency
#
# def call(some_argument)
# important_dependency.call(:some_argument)
# rescue StaqExtraction::Defect => e
# yield "could not do awesome thing: #{e.message}"
# end
#
# private
#
# # Note we can still refer to ImportantDependency by name here; we make it easy to override
# # but also provide the most common default value
# def important_dependency
# @important_dependency ||= ImportantDependency.new
# end
# end
#
# Don't just use "describe Widget" here, because in RSpec 3+ and in all of our newer codebases, we
# disable monkey-patching mode, which means you need to send explicit messages to the RSpec
# object. Many of our own DSL problems stem from the fact that we use too much meta-programming magic.
RSpec.describe Widget do
# 1. VARIABLES AND DEPENDENCIES
#
# Start with all of the variables/dependencies your unit test will need. These should be built
# in a way that assumes all of the other classes don't exist yet, so this test can run
# independently. Having a whole lot of let statements is a code smell, indicating your design
# may be bloated, and that there are more classes hiding inside the one you are testing.
#
# We want to make our setup code completely separate from our assertion code, which makes the
# tests easier to read and failures easier to interpret.
let(:dependency_result) { double(:dependency_result) }
let(:important_dependency) do
double(:important_dependency,perform: dependency_result)
end
let(:condition_we_care_about) { true }
let(:argument_for_call_method) do
double(:argument_for_call_method,condition?: condition_we_care_about)
end
# 2. OBJECT INSTANTIATION
#
# The subject block is only necessary if there are initializer arguments for your class
#
# Otherwise RSpec does this for you:
# subject do
# Widget.new
# end
# So far example if Widget#initialize has an argument:
# subject do
# Widget.new(tmp_dir_path)
# end
# 3. PRECONDITIONS
#
# Finally, you setup and preconditions your objects need, so that this test can run indepedently of all other code.
# If there are a lot of before blocks, that's also a code smell, indicating your code knows too much about what
# other objects are doing. When using 3rd party code this is hard to avoid unless that code is well-designed.
before do
subject.important_dependency = important_dependency
end
# 4. PRIMARY BEHAVIOR ASSERTIONS
#
# Now we add assertions for all of the behavior for the most important/happy code path, the one withotu
# special corner cases or conditions. Code that causes a side-effect is easier to test, easier to debug,
# and overall better, so we focus on those kinds of tests
it "our method causes the side effect we care about" do
subject.call(:abc)
expect(important_dependency).
to have_received(:perform).
with(:abc)
end
# We try hard not to write code that uses return values (objects should tell each other what to do, not ask each other questions),
# but sometimes it's just way easier/more clear to use return values. In which case you have to test the results
it "our method returns the value we want" do
expect(subject.call(:abc)).to eq(dependency_result)
end
# 5. CORNER CASES AND SPECIAL BEHAVIOR ASSERTIONS
#
# Now we test corner cases. We try to avoid conditionals where possible, but again, that's sometimes the most
# clear/expeditious way to get the job done. You just need to be careful to test both legs of a conditional.
# We wrap these tests in a context blocg
context "when important condition is false" do
# Note how elegantly we can create the exact test scenario in question, by overriding the initial setup conditions
let(:condition_we_care_about) { false }
# VERY important to test that the side-effect DOESN'T happen
it "our method doesn't cause a side effect" do
subject.call(:abc)
expect(important_dependency).
not_to have_received(:perform)
end
# It may also make sense to add an additional return value test
end
# 6. ERROR AND EXCEPTION HANDLING
#
# In some ways these the most important tests of all. The worst bugs to encounter are ones that occur in
# error-handling logic, since they obscure the true failure
context "with important dependency raising StaqExtraction::Defect" do
before do
allow(important_dependency).
to receive(:perform).
and_raise(StaqExtraction::Defect)
end
# This is always worth testing explicitly, because you get a better error message
it "does not raise error" do
expect {
subject.call(:abc)
}.not_to raise_error
end
it "yields an error message back to the caller" do
# Very common pattern in our codebase. Note that I don't care what error message
# gets yielded back. I just care that _a_ string gets yielded. Unit tests should
# almost never assert anything about string contents, unless that's the main
# point of the method
expect { |b|
subject.call(:abc,&b)
}.to yield_with_args(an_instance_of(String))
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment