Carefully balancing his tray so as not to spill the cup of soup, C01t walks slowly over to his teams lunch table. As he sets the tray down on the table he hears Kaya say: “Ian, let me tell you why I love rspec
and why I look for similar test frameworks in other programming languages!” C01t wonders what this is all about. Leaning over so as to not disturb the ongoing conversation he asks Nick: “What is rspec
?”. “rspec
is the BDD test framework for Ruby“, replies Nick. C01t settles into his seat, as Kaya starts to lay out her argument. This is one debate he does not want to miss.
Nested test groups
rspec
‘s DSL is a powerful tool for organizing test cases, aka examples. You can declare test groups with the methods describe
and context
. Use test groups to associate tests that verify related functionality or share the same execution context. Furthermore, you can nest test groups. Nested groups are essentially sub classes of the outer groups and provide the expected inheritance semantics. There is no limit on the depth of the nesting.
To illustrate how to best use the describe
and context
, let’s consider the following class:
class Froyo
def add_toppings(toppings)
...
end
def price(coupon)
...
end
end
First you define a describe
with the Froyo
as the parameter to identify the class under test. Then, for each method of Froyo
you add a nested describe
with a string containing the method name as the parameter. (Instance method names should be preceded by "#"
. Class method names should be preceded by "."
.) Finally, we define a nested context
for each relevant scenario.
You can implement tests inside any of the describe
or context
blocks. As a result, when reading this spec you can quickly identify what functionality and scenario is being tested. Additionally, when you need to add additional tests for any of the methods it is obvious where to insert them.
The complete spec file for the Froyo
class would look something like:
describe Froyo do
describe "#add_toppings" do
# add scenarios and test cases
end
describe "#price" do
it "returns a number >= 0" do
# some test code
end
context "with no toppings" do
it "costs $4.5" do
# some test code
end
end
context "with 2 toppings" do
it "costs $5.0" do
# some test code
end
it "cost $4.5 with free toppings coupon" do
# some test code
end
end
# more contexts
end
end
Hierarchical before
and after
Like most other test frameworks, rspec
provides before
and after
hooks for performing setup and tear down. And similarly, you can scope the hooks to either a single test case (before(:each)
and after(:each)
), a group of test cases (before(:all)
and after(:all)
), or the entire run (before(:suite)
and after(:suite)
).
But what distinguishes rspec
from most other test frameworks, is that you can define hooks inside any test group as well as in a rspec
configure (a global configuration section). And for a given test, rspec
will find and execute all applicable setup and tear down methods. Therefore, you can decompose test setup and tear down cleanly between the nested test groups.
Finally, consider the following example spec:
describe Froyo do
before(:all) { puts "Froyo one-time setup" }
after(:all) { puts "Froyo one-time tear down" }
before(:each) { puts "Froyo setup" }
after(:each) { puts "Froyo tear down" }
describe "#price" do
before(:all) { puts "price one-time setup" }
after(:all) { puts "price one-time tear down" }
before(:each) { puts "price setup" }
after(:each) { puts "price tear down" }
it "costs at least $0" do
puts "costs at least $0"
end
it "validates the type of the coupon parameter" do
puts "validates the type of the coupon parameter"
end
# more scenarios and tests
end
end
If you run the above spec file, you will get the following output:
Froyo one-time setup price one-time setup Froyo setup price setup costs at least $0 price teardown Froyo tear down Froyo setup price setup validates the type of the coupon parameter price teardown Froyo tear down price one-time teardown Froyo tear down
rspec
invoked all relevant setup and tear down blocks, without blocks nested in the same group as the tests explicitly referring to blocks in the parent groups.
Memoized subject
and let
helpers
You can use subject
and let
declarations inside test groups to replace local test variables with methods whose return values are memoized. The values returned by the subject
and let
declarations are allocated on first use. Since the return values are memoized, the methods can be used repeatedly within a test.
describe Froyo do
subject(:froyo) { described_class.new }
let(:toppings) { %w[sprinkles 'gummy bears'] }
it "costs $4.50 without toppings" do
expect(froyo.price).to eq(4.50)
end
it "costs $5.00 with 2 toppings" do
froyo.add_toppings toppings
expect(froyo.price).to eq(5.00)
end
end
So the first test above does not incur the penalty of allocating the toppings array. Furthermore, the second test calls froyo
twice and received the same object.
Additionally, both declarations play well with nested test groups, and before(:each)
and after(:each)
hooks. When executing a test or a before
/after
hook referencing a method declared via subject
or let
, rspec
searches the test group hierarchy and invokes the method closest to the test.
describe Froyo do
subject(:froyo) { described_class.new }
let(:toppings) { [] }
before { froyo.add_toppings toppings }
it "costs $4.50 without toppings" do
expect(froyo.price).to eq(4.50)
end
context "with 2 toppings" do
let(:toppings) { %w[sprinkles 'gummy bears'] }
it "costs $5.00 with 2 toppings" do
expect(froyo.price).to eq(5.00)
end
end
# more scenarios and tests
end
First, you used froyo
and toppings
inside a before(:each)
hook. Then you overwrote let(:toppings)
inside the context "with 2 toppings"
. And during execution, for the test inside context "with 2 toppings"
when the before
hook in the parent group was executed the overridden value of toppings
was used.
Finally, putting it all together you can DRY test code using subject
and let
:
describe Froyo do
subject(:froyo) { described_class.new }
let(:toppings) { [] }
before { froyo.add_toppings toppings }
describe "#price" do
let(:coupon) { "FREE_TOPPINGS" }
context "with no toppings" do
it "costs $4.50" do
expect(froyo.price).to eq(4.50)
end
it "costs $4.50 with a free toppings coupon" do
expect(froyo.price(coupon)).to eq(4.50)
end
end
context "with 2 toppings" do
let(:toppings) { %w[sprinkles 'gummy bears'] }
it "costs $5" do
expect(froyo.price).to eq(5.00)
end
it "costs $4.50 with a free toppings coupon" do
expect(froyo.price(coupon)).to eq(4.50)
end
end
# more scenarios and tests
end
end
Test case reuse with shared_examples
Finally, you can use the method shared_examples
to define test groups that can be nested into multiple other test groups. The shared test groups are scoped based of where they are defined. Therefore, they are available to for inclusion in the group they were defined in or child groups, but not in sibling or parent groups.
You include a shared test group to be evaluated in the context of another test group using the it_behaves_like
method.
Consequently, you can use shared groups to execute a common set of tests for each scenario of the price
method of the Froyo
class:
describe Froyo do
subject(:froyo) { described_class.new }
let(:toppings) { [] }
before { froyo.add_toppings toppings }
describe "#price" do
shared_examples "real price" do
it "costs at least $0" do
expect(froyo.price).to be >= 0.0
end
end
context "with no toppings" do
it_behaves_like "real price"
end
context "with 2 toppings" do
let(:toppings) { %w[sprinkles 'gummy bears'] }
it_behaves_like "real price"
end
# more scenarios and tests
end
end
Additional Resources
Probably the two most useful resources when using rspec
are:
the official documentation and better specs. Most of all, you should review better specs before writing any tests.
In addition all the code in this post is available in our lab-ruby repository.
Hello.This post was really interesting, particularly because I was browsing for thoughts on this topic last Saturday.
Hello everybody ! can anyone suggest where I can purchase All CBD Vape?
I have noticed that credit restoration activity needs to be conducted with techniques. If not, you might find yourself damaging your rating. In order to realize your aspirations in fixing to your credit rating you have to see to it that from this moment you pay your monthly dues promptly in advance of their planned date. It’s really significant on the grounds that by certainly not accomplishing so, all other methods that you will decide on to improve your credit ranking will not be powerful. Thanks for revealing your thoughts.