Created
August 30, 2016 14:14
-
-
Save subelsky/bf1a02690576ce1f2dfa68fba0d15656 to your computer and use it in GitHub Desktop.
RSpec Template explaining basic test setup
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
# 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