Let’s Test It Well: Simply and Smartly

mock_model
and stub_model
is, how to use shared_context
and shared_examples
, etc.If you are fond of testing, just like our Ruby Developer Nastia Shaternik, you’ll probably be interested to read her post about using RSpec. There, she dwells on how some RSpec features that are not commonly used can help you to simplify testing and make tests clearer.
This article explains how to make tests readable as short documentation and how using mock_model can make your tests run faster. In addition, we exemplify the usage of RSpec’s built-in expectations, two strategies of sharing the same data among different examples, etc.
Tests as specification
I’m really fond of tests, which can be read as short documentation and expose the application’s API. To help you to cope with it, run your specs with the --format d[ocumentation]
option. The output will be printed in a nested way. If you don’t understand what your code can do, you should rewrite your tests. In order not to write RSpec options every time when running specs, create a .rspec
configuration file in your home or application directory. (Options that are stored in ./.rspec
take precedence over options stored in ~/.rspec
, and any options declared directly on the command line will take precedence over those in either file.) The .rspec
file will look as shown below.
--color
--format d[ocumentation]
RSpec’s built-in expectations
Avoid using !=
and remember about should_not
. To test the actual.predicate?
methods, use actual.should be_[predicate]
.
actual.should be_true # passes if actual is truthy (not nil or false)
actual.should be_false # passes if actual is falsy (nil or false)
actual.should be_nil # passes if actual is nil
actual.should be # passes if actual is truthy (not nil or false)
Please also use collection’s matchers.
actual.should include(expected)
actual.should have(n).items
actual.should have_exactly(n).items
actual.should have_at_least(n).items
actual.should have_at_most(n).items
mock_model
vs. stub_model
By default, mock_model
produces a mock that acts like an existing record (persisted()
returns true). The stub_model
method is similar to mock_model
except that it creates an actual instance of the model. This requires that the model has a corresponding table in the database. So, the main advantage is obvious, tests written with mock_model
, will run faster. Another advantage of mock_model
over stub_model
is that it’s a true double, so the examples are not dependent on the behavior/misbehavior or even the existence of any other code.
Usage of the mock_model
method is quite simple and is illustrated below.
describe SpineData do
let(:user) { create :user }
before(:each) do
SpineData.stub_chain(:controller, :current_user).and_return(user)
end
# ...
context "dealing with content set" do
let(:set) { mock_model ContentSet }
describe ".set_updater" do
subject { SpineData.set_updater set }
it { should_not be_blank }
its([:id]) { should == set.id}
end
end
end
subject
and it {}
In an example group, you can use the subject
method to define an explicit subject for testing by passing it a block. Now, you can use the it {}
constructions to specify matchers. It’s just concise!
describe AccountProcessing do
include_context :oauth_hash
# ...
context 'user is anonymous' do
let(:acc_processing) { AccountProcessing.new(auth_hash) }
# ...
describe '#create_or_update_account' do
subject { acc_processing.create_or_update_account }
it { should be_present }
end
describe '#account_info' do
subject { acc_processing.account_info }
it 'returns valid account info hash' do
should be == { network: auth_hash['provider'],
email: auth_hash['info']['email'],
first_name: auth_hash['info']['first_name'],
last_name: auth_hash['info']['last_name'],
birthday: nil
}
end
end
end
end
DRY!ness
There are two strategies—shared context and shared examples—to share the same data among different examples.
Shared context
Use shared_context
to define a block that will be evaluated in the context of example groups by employing include_context
. You can put settings (something in the before
/after
block), variables, data, and methods. All the things you put into shared_contex
will be accessible in the example group by name.
Below, you can see how to define shared_context
.
shared_context "shared_data" do
before { @some_var = :some_value }
def shared_method
"it works"
end
let(:shared_let) { {'arbitrary' => 'object'} }
subject do
'this is the shared subject'
end
end
Here is how you use shared_context
.
require "./shared_data.rb"
describe "group that includes a shared context using 'include_context'" do
include_context :shared_data
it "has access to methods defined in shared context" do
shared_method.should eq("it works")
end
it "has access to methods defined with let in shared context" do
shared_let['arbitrary'].should eq('object')
end
it "runs the before hooks defined in the shared context" do
@some_var.should be(:some_value)
end
it "accesses the subject defined in the shared context" do
subject.should eq('this is the shared subject')
end
end
Shared examples
Shared examples are used to describe a common behavior and encapsulate it into a single example group. Then, examples can be applied to another example group.
This is how you define shared_examples
.
require "set"
shared_examples "a collection" do
let(:collection) { described_class.new([7, 2, 4]) }
context "initialized with 3 items" do
it "says it has three items" do
collection.size.should eq(3)
end
end
describe "#include?" do
context "with an an item that is in the collection" do
it "returns true" do
collection.include?(7).should be_true
end
end
context "with an an item that is not in the collection" do
it "returns false" do
collection.include?(9).should be_false
end
end
end
end
Below, you can see how to use shared_examples
.
describe Array do
it_behaves_like "a collection"
end
describe Set do
it_behaves_like "a collection"
end
For more information about RSpec, you can consult with the official documentation. The book by RSpec’s creator David Chelimsky will also be helpful. Or, you can opt for these guides on Better Specs.