This week I struggled with conceptualizing and expressing useful tests in RSpec. After getting past the basic DSL of RSpec, describe
and it
blocks, I would find myself lost about how to express my expectations.
My first instinct when I’m struggling with a concept is to read all the material I can find online and do any tutorials I can get my hands on, but everything I found on RSpec referred to the deprecated “should” syntax. I didn’t want to further confuse the issue in my head by reading materials that demonstrated a deprecated syntax. So I embarked on a trial-and-error process of building a simple class with some reasonably thorough tests.
I found a few resources and tricks that helped me get a handle on the subject.
Tests are just expect() paired with a “matcher”
Behaviour is asserted by pairing
expect().to
andexpect().not_to
with a Matcher predicate.
expect(a_result).to eq("this value")
expect(a_result).not_to eq("that value")
We have typically used the eq()
method as a matcher, but it’s useful to know the other slightly different equality measures.
eq(expected) # same value
eql(expected) # same value and type
equal(expected) # same exact object
Basic matchers cheatsheet: https://learn.thoughtbot.com/test-driven-rails-resources/matchers.pdf
More matchers in RSpec docs: http://rubydoc.info/gems/rspec-expectations/file/README.md#Built-in_matchers
Fun with RSpec
If I see another artist or movie spec, I might jump out a window, so I decided to build a really simple little model of something I enjoy, gin.
class Gin
attr_accessor :name, :style, :notes
GINS = []
def initialize
GINS << self
end
def self.all
GINS
end
def self.reset_all
GINS.clear
end
end
And now to the fun part, writing the tests.
describe 'Gin Attributes' do
let(:tanqueray) { Gin.new }
it 'can have a name' do
tanqueray.name = "Tanqueray"
expect(tanqueray.name).to eq("Tanqueray")
end
it 'can have a style' do
tanqueray.style = "London Dry"
expect(tanqueray.style).to eq("London Dry")
expect(tanqueray.style).to match(/London Dry/)
end
it 'can have multiple notes of flavor' do
notes = ["angelica root","liquorice","juniper","coriander"]
tanqueray.notes = ["juniper","coriander","angelica root","liquorice"]
expect(tanqueray.notes.length).to eq(4)
expect(tanqueray.notes).to match_array(notes)
expect(tanqueray.notes).to have(4).notes
end
end
match()
is great for when you need a regex. I also found match_array()
useful. I especially like the readability of expect(tanqueray.notes).to have(4).notes
. The addition of .notes
at the end is not required and is pure sugar.
From there, I moved on to a new describe
block to spec out my class methods. In addition to the standard eq()
, I used a comparison operator, which is also supported.
describe 'Gin class methods' do
before(:each) do
Gin.all.clear
end
it 'can list all Gins' do
hendricks = Gin.new
expect(Gin.all.length).to eq(1)
barr_hill = Gin.new
expect(Gin.all.count).to be > 1
end
it 'can reset the list of Gins' do
gins = [Gin.new, Gin.new]
Gin.reset_all
expect(Gin.all.length).to eq(Gin.all.clear.length)
end
end
I didn’t get as far as I had intended with these tests, so I hope to follow this up with a further exploration of test set up and tear down. I had a tough time getting that to work elegantly so think I still have much to learn there.
Satisfy and custom matchers
If the standard matchers don’t work well for a given scenario, you can also use satisfy
to get a little more manual with it, and RSpec also allows for custom matchers to be defined.
satisfy is valid for objects and blocks, and allows the target to be tested against an arbitrarily speciļ¬ed block of code.
it 'can have multiple notes of flavor' do
expect(tanqueray.notes).to satisfy {|n| n.count == 4}
end
Other points I made note of along the way:
Add --format documentation
to your .rspec file after you run rspec --init
; this is helpful for the test writing process.
This was the most useful cheatsheet I found and is referenced throughout this article. https://www.anchor.com.au/wp-content/uploads/rspec_cheatsheet_attributed.pdf