Basic Usage
RSpec tests can be written in several flavors (or “styles”). Let’s have a look at the most basic one first.
RSpec wants us to define tests in a file that ends with _spec.rb
, so we store
both our class and our test in the file user_spec.rb
. Normally, in modern
code bases, you’d store your code in one file, and your tests in another file:
require "date"
def leap_year?(year)
year % 400 == 0 or year % 100 != 0 and year % 4 == 0
end
class User
def initialize(name, birthday)
@name = name
@birthday = birthday
end
def name
@name
end
def born_in_leap_year?
leap_year?(Date.parse(@birthday).year)
end
end
describe User do
it "is born in a leap year when born in 2000" do
user = User.new("Francisca", "2000-01-01")
actual = user.born_in_leap_year?
expected = true
expect(actual).to eq expected
end
end
Does that read ok?
We’re going to work with this test more later, so let’s shorten that a bit and
use less space, by removing the actual
and expected
variables:
describe User do
it "is born in a leap year when born in 2000" do
user = User.new("Francisca", "2000-01-01")
expect(user.born_in_leap_year?).to eq true
end
end
Ok. That’s the same, but uses 2 lines instead of 4.
Remember how Sinatra is a DSL, a language, for “talking” about (writing code that deals with) the problem domain of HTTP, i.e. writing web applications?
RSpec is a DSL for the problem domain of writing tests (or “specifications”).
While Sinatra defines methods such as get
, post
, status
, redirect
, and
so on, RSpec defines methods like describe
, it
, expect
, and eq
(equal).
Using these methods we can describe our expectations about our code and
execute them. In RSpec’s thinking, that’s what tests are all about: expressing
our expectations about the behaviour of our code. We describe the class
User
, and specify our expectations.
Instead of it
you can also use example
. That’s exactly the same:
describe User do
example "is born in a leap year when born in 2000" do
# ...
end
end
Also, suppose we have many tests that deal with the case that a user was born in 2000, maybe like this:
describe User do
it "is born in a leap year when born in 2000" do
# ...
end
it "is at voting age when born in 2000" do
# ...
end
end
RSpec allows us to group such tests (examples) like so:
describe User do
describe "when born in 2000" do
it "is born in a leap year" do
# ...
end
example "is at voting age" do
# ...
end
end
end
And again, there’s an alias for nested describe
blocks — you can use context
there, too:
describe User do
context "when born in 2000" do
it "is born in a leap year" do
# ...
end
example "is at voting age" do
# ...
end
end
end
Nice, isn’t it? Our spec says: “A user, in the context of being born in 2000, is born in a leap year”, and then “[in the same context] is at voting age”.
In short, the methods describe
and context
are used to set up a logical
structure for your tests. There needs to be at least one top level describe
block. This is the equivalent to defining a class that inherits from
Minitest::Test
.
The method it
(or one of its alias example
and specify
) is then used to
add the actual tests, i.e. that’s the equivalent to defining methods that start
with test_
in Minitest.
Under the hood RSpec uses a lot of metaprogramming;
i.e. RSpec has methods that, when called, define code, classes and methods,
according to the arguments you pass. For example the code describe User do ...
end
defines a class, and methods like context
, and it
add more code to
this class. RSpec then, eventually, executes this code automatically, and runs
your tests.
That means, even though you’re very familiar with Ruby, you’ll still need to learn RSpec in order to use it effectively. That’s one of the reasons why some Ruby developers dislike RSpec: It’s not “just Ruby” any more. On the flip side, it’s extremely powerful, and comes with features that no other testing library has.
When you run the code in our user_spec.rb
file, the output will look
something like this:
$ rspec user_spec.rb
.
Finished in 0.00204 seconds (files took 0.1559 seconds to load)
1 example, 0 failures
The dot indicates that there is exactly one test defined. RSpec calls tests “examples”. That’s because they like to stress that tests shouldn’t be so much about technical details, but about the behavior that the user cares about. They like to say that we “specify” behavior by the way of defining “examples”.
Let’s break our test, and change the method born_in_leap_year?
to always
return false
:
def born_in_leap_year?
false
end
When you now run the code again the output will look like this:
$ rspec user_spec.rb
F
Failures:
1) User born in 2000 is born in a leap year
Failure/Error: expect(user.born_in_leap_year?).to eq true
expected: true
got: false
(compared using ==)
# ./user_spec.rb:25:in `block (3 levels) in <top (required)>'
Finished in 0.02033 seconds (files took 0.15813 seconds to load)
1 example, 1 failure
Failed examples:
rspec ./user_spec.rb:23 # User born in 2000 is born in a leap year
Wow, that’s pretty comprehensive. RSpec tells us exactly what’s going wrong, and where. So nice of them.