© Panos Matsinopoulos 2020
P. MatsinopoulosPractical Test Automationhttps://doi.org/10.1007/978-1-4842-6141-5_4

4. Introduction to RSpec

Panos Matsinopoulos1 
(1)
KERATEA, Greece
 
RSpec advertises as being the tool that makes TDD (Test-Driven Development) and BDD (Behavior-Driven Development) fun. This is an introduction to RSpec (Figure 4-1) that will give you enough knowledge to write your first Ruby application and cover it with RSpec specifications – or, actually, since you are doing TDD, to first write the specifications and then implement the application. It is the foundation for the next chapters that deal with more advanced concepts of RSpec and testing in general.
../images/497830_1_En_4_Chapter/497830_1_En_4_Fig1_HTML.jpg
Figure 4-1

Introduction to RSpec

Learning Goals

  1. 1.

    Learn about installing RSpec.

     
  2. 2.

    Learn about getting the version of RSpec installed.

     
  3. 3.

    Learn how to initialize your project to use RSpec.

     
  4. 4.

    Learn how the RSpec configuration is set up.

     
  5. 5.

    Learn about the structure of the files/folders related to RSpec.

     
  6. 6.

    Learn about the conversational pattern of RSpec.

     
  7. 7.

    Learn about the example groups.

     
  8. 8.

    Learn about the examples.

     
  9. 9.

    Learn about the Ruby blocks that are sent to describe and it methods.

     
  10. 10.

    Learn how to execute the RSpec suite.

     
  11. 11.

    Learn about the expectations.

     
  12. 12.

    Learn about the documentation formatter.

     
  13. 13.

    Learn how to follow the TDD workflow to develop your application.

     
  14. 14.

    Learn about the different phases of a test.

     
  15. 15.

    Learn about the exception that is raised when RSpec expectation fails.

     
  16. 16.

    Learn about hooks.

     
  17. 17.

    Learn about helper methods.

     
  18. 18.

    Learn about let.

     
  19. 19.

    Learn about memoization, when it is useful, and when it is tricky.

     
  20. 20.

    Learn how to use contexts.

     
  21. 21.

    Learn how to run a dry run of your specs.

     
  22. 22.

    Learn how to run specs in a specific file.

     
  23. 23.

    Learn how to run a specific example from a specific file.

     
  24. 24.

    Learn how RubyMine integrates with RSpec.

     

Introduction

You will first install RSpec. RSpec is coming in the form of four gems:
  1. 1.

    rspec-support

     
  2. 2.

    rspec-core

     
  3. 3.

    rspec-expectations

     
  4. 4.

    rspec-mocks

     

There is also the gem rspec that, when installed, installs all the other gems. And this is the gem that you are going to install here.

New Project

Let’s create a new project, named coffee_shop. It needs to be a Ruby project with Ruby 2.x installed. Start that project in RubyMine. Moreover, make sure that you have the rbenv integration with the .ruby-version file specified having the version of the Ruby you are using. For example, my .ruby-version file, in the root folder of my project, has the content 2.6.5 (Figure 4-2).
../images/497830_1_En_4_Chapter/497830_1_En_4_Fig2_HTML.jpg
Figure 4-2

RubyMine coffee_shop Project with the .ruby-version File

Then create the file Gemfile with the following content (Listing 4-1).
# File: Gemfile
#
source 'https://rubygems.org'
gem 'rspec'
gem 'rake'
Listing 4-1

Gemfile

The preceding Gemfile specifies project dependencies on the rspec and rake gems. Let’s do bundle to install them:
$ bundle
Fetching gem metadata from https://rubygems.org/..........
Resolving dependencies...
Using rake 13.0.1
Installing diff-lcs 1.3
Installing rspec-support 3.9.2
Using bundler 2.1.4
Installing rspec-core 3.9.1
Installing rspec-expectations 3.9.1
Installing rspec-mocks 3.9.1
Installing rspec 3.9.0
Bundle complete! 2 Gemfile dependencies, 8 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
$
Here is a screenshot of your project in RubyMine (Figure 4-3).
../images/497830_1_En_4_Chapter/497830_1_En_4_Fig3_HTML.jpg
Figure 4-3

Project in RubyMine

rspec Version

Now that you have RSpec installed, you can invoke the main binary that is shipped with RSpec, the rspec, and get the version of RSpec installed as a confirmation that you have a proper installation. Run the following command:
$ bundle exec rspec --version
RSpec 3.9
  - rspec-core 3.9.1
  - rspec-expectations 3.9.1
  - rspec-mocks 3.9.1
  - rspec-support 3.9.2
$

You can see that the version of RSpec at the time of writing is 3.9. You may have a later version installed on your machine. But any version 3.x will do.

Initialize RSpec

Having installed the rspec gem, the next step that you have to do is to initialize your project in order to be ready to use RSpec. Run the following command:
$ bundle exec rspec --init
  create   .rspec
  create   spec/spec_helper.rb
$
The rspec --init command created two files:
  1. 1.

    The .rspec file that contains default running options for the command-line test runner

     
  2. 2.

    The spec/spec_helper.rb file that will be loaded before every test run session, thanks to the --require spec_helper line that exists inside the .rspec file

     

RSpec Configuration

The spec/spec_helper.rb file initially contains a call to the RSpec.configure method that is used to configure the RSpec behavior. RSpec comes with some sensible defaults, and if you read the comments in the spec/spec_helper.rb file, you can switch on/off anything that you feel like doing so.

My recommendation, to start with, is to switch on the random execution of the tests (as you had with minitest). Hence, please, make sure that
  config.order = :random
  Kernel.srand config.seed

are not commented out.

And another recommendation is to disable monkey patching. Make sure that the following line is not commented out:
  config.disable_monkey_patching!
Ruby Comments

Please, remember that the lines between =begin and =end are considered comments and not actual Ruby statements.

Here, I give you the contents of the spec/spec_helper.rb file as it has to be, after having applied the changes that I have recommended in the preceding text (Listing 4-2).
# This file was generated by the `rspec --init` command. Conventionally, all
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
# The generated `.rspec` file contains `--require spec_helper` which will cause
# this file to always be loaded, without a need to explicitly require it in any
# files.
#
# Given that it is always loaded, you are encouraged to keep this file as
# light-weight as possible. Requiring heavyweight dependencies from this file
# will add to the boot time of your test suite on EVERY test run, even for an
# individual file that may not need all of that loaded. Instead, consider making
# a separate helper file that requires the additional dependencies and performs
# the additional setup, and require it from the spec files that actually need
# it.
#
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
RSpec.configure do |config|
  # rspec-expectations config goes here. You can use an alternate
  # assertion/expectation library such as wrong or the stdlib/minitest
  # assertions if you prefer.
  config.expect_with :rspec do |expectations|
    # This option will default to `true` in RSpec 4. It makes the `description`
    # and `failure_message` of custom matchers include text for helper methods
    # defined using `chain`, e.g.:
    #     be_bigger_than(2).and_smaller_than(4).description
    #     # => "be bigger than 2 and smaller than 4"
    # ...rather than:
    #     # => "be bigger than 2"
    expectations.include_chain_clauses_in_custom_matcher_descriptions = true
  end
  # rspec-mocks config goes here. You can use an alternate test double
  # library (such as bogus or mocha) by changing the `mock_with` option here.
  config.mock_with :rspec do |mocks|
    # Prevents you from mocking or stubbing a method that does not exist on
    # a real object. This is generally recommended, and will default to
    # `true` in RSpec 4.
    mocks.verify_partial_doubles = true
  end
  # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
  # have no way to turn it off -- the option exists only for backwards
  # compatibility in RSpec 3). It causes shared context metadata to be
  # inherited by the metadata hash of host groups and examples, rather than
  # triggering implicit auto-inclusion in groups with matching metadata.
  config.shared_context_metadata_behavior = :apply_to_host_groups
  # The settings below are suggested to provide a good initial experience
  # with RSpec, but feel free to customize to your heart's content.
  # This allows you to limit a spec run to individual examples or groups
  # you care about by tagging them with `:focus` metadata. When nothing
  # is tagged with `:focus`, all examples get run. RSpec also provides
  # aliases for `it`, `describe`, and `context` that include `:focus`
  # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
  # config.filter_run_when_matching :focus
  # Allows RSpec to persist some state between runs in order to support
  # the `--only-failures` and `--next-failure` CLI options. We recommend
  # you configure your source control system to ignore this file.
  # config.example_status_persistence_file_path = "spec/examples.txt"
  # Limits the available syntax to the non-monkey patched syntax that is
  # recommended. For more details, see:
  #   - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
  #   - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
  #   - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
  config.disable_monkey_patching!
  # This setting enables warnings. It's recommended, but in some cases may
  # be too noisy due to issues in dependencies.
  # config.warnings = true
  # Many RSpec users commonly either run the entire suite or an individual
  # file, and it's useful to allow more verbose output when running an
  # individual spec file.
  #if config.files_to_run.one?
  #  # Use the documentation formatter for detailed output,
  #  # unless a formatter has already been configured
  #  # (e.g. via a command-line flag).
  #  config.default_formatter = "doc"
  #end
  # Print the 10 slowest examples and example groups at the
  # end of the spec run, to help surface which specs are running
  # particularly slow.
  # config.profile_examples = 10
  # Run specs in random order to surface order dependencies. If you find an
  # order dependency and want to debug it, you can fix the order by providing
  # the seed, which is printed after each run.
  #     --seed 1234
  config.order = :random
  # Seed global randomization in this process using the `--seed` CLI option.
  # Setting this allows you to use `--seed` to deterministically reproduce
  # test failures related to randomization by passing the same `--seed` value
  # as the one that triggered the failure.
  Kernel.srand config.seed
end
Listing 4-2

spec/spec_helper.rb with RSpec Recommended Configuration

Inside the spec Folder

When you worked with minitest, your tests lived inside the folder test. RSpec assumes that all your tests are going to live inside the folder spec, or any of its subfolders. Also, the files that are going to be considered test files are the ones that will have a filename ending in _spec.rb. Hence, the file spec/spec_helper.rb is not considered a file with tests, whereas, if we had the file with the name spec/customer_spec.rb, this would have been considered a file with tests.

Describing

Let’s assume that you want to model the ideal sandwich, just to start with something more fun.

RSpec is using the words describe and it to express concepts of your model in a conversational format.

This could have been the start of a dialog, a conversation, about the ideal sandwich:
  • Describe an ideal sandwich.

  • First, it is delicious.

See this dialog in Figure 4-4.
../images/497830_1_En_4_Chapter/497830_1_En_4_Fig4_HTML.png
Figure 4-4

describe it Conversation

Let’s create the file spec/sandwich_spec.rb with the following content (Listing 4-3).
# File: spec/sandwich_spec.rb
#
RSpec.describe 'An ideal sandwich' do
  it 'is delicious' do
    expect(true).to eq(false)
  end
end
Listing 4-3

spec/sandwich_spec.rb

This is a spec file. I use the word spec or specification rather than test when I am in the context of RSpec.

In fact, when you are in the RSpec context, you say that the spec file contains/defines example groups. Each example group is defined using the RSpec.describe call.

The first argument to the RSpec.describe call is the documentation (or description or name) string for the particular example group. The do ... end block that is sent to RSpec.describe defines the body of the example group. The body usually defines one or more examples. Each example is defined with the call to method it. An example group may have many calls to it, and each one is going to be a different example in the example group. The first argument to it is the documentation (or description or name) string for the particular example. The block do ... end that is sent to it is the actual implementation of the example (Figure 4-5).
../images/497830_1_En_4_Chapter/497830_1_En_4_Fig5_HTML.jpg
Figure 4-5

Example Groups and Examples

Sanity Check

Now that you have the basics explained and the first spec file in place, let’s execute the RSpec runner:
$ bundle exec rspec
Randomized with seed 63375
F
Randomized with seed 6735
F
Failures:
  1) An ideal sandwich is delicious
     Failure/Error: expect(true).to eq(false)
       expected: false
            got: true
       (compared using ==)
       Diff:
       @@ -1,2 +1,2 @@
       -false
       +true
     # ./spec/sandwich_spec.rb:5:in `block (2 levels) in <top (required)>'
Finished in 0.05128 seconds (files took 0.25256 seconds to load)
1 example, 1 failure
Failed examples:
rspec ./spec/sandwich_spec.rb:4 # An ideal sandwich is delicious
Randomized with seed 6735
$
The suite has run successfully, that is, without any errors. But you have one failure. Do you see the output line 1 example, 1 failure? The RSpec runner (bundle exec rspec) identified the single example defined in your suite and executed it. However, this failed with the failure
1) An ideal sandwich is delicious
     Failure/Error: expect(true).to eq(false)
       expected: false
            got: true
       (compared using ==)
       Diff:
       @@ -1,2 +1,2 @@
       -false
       +true
     # ./spec/sandwich_spec.rb:5:in `block (2 levels) in <top (required)>'
It is clearly mentioning that spec/sandwich_spec.rb failed at line 5: expect(true).to eq(false). This seems to be quite easy to explain (Figure 4-6).
../images/497830_1_En_4_Chapter/497830_1_En_4_Fig6_HTML.jpg
Figure 4-6

Expected vs. Actual

The example line that exercises the expectation of the specification, expect(true).to eq(false), is mentioning that the value true, which is the actual value, needs to be equal to the value false, which is the expected value. And since true is not equal to false, the specification fails when evaluated by the RSpec runner.

Easy to explain and easy to fix. The following is the new version of the spec/sandwich_spec.rb file (Listing 4-4).
# File: spec/sandwich_spec.rb
#
RSpec.describe 'An ideal sandwich' do
  it 'is delicious' do
    expect(true).to eq(true)
  end
end
Listing 4-4

New Version of the spec/sandwich_spec.rb File

If you save and run the RSpec runner again, you will get this:
$ bundle exec rspec
Randomized with seed 4291
.
Finished in 0.00097 seconds (files took 0.11265 seconds to load)
1 example, 0 failures
Randomized with seed 4291
$

Nice! This is reporting 1 example, 0 failures. You can see the green . implying one example has been successfully evaluated.

Documentation Formatter

The RSpec runner comes with a very useful results formatter. It prints out the specs (each describe and it documentation string) in a nested/tree-like format which finally works as a very good documentation of your project specifications.

Let’s try that with the current specs that you have:
$ bundle exec rspec --format doc
Randomized with seed 6501
An ideal sandwich
  is delicious
Finished in 0.00081 seconds (files took 0.07047 seconds to load)
1 example, 0 failures
Randomized with seed 6501
$
Do you see the
An ideal sandwich
  is delicious

part of the output? These are the describe and it documentation/name strings in your spec file. You can also see that the example that ran successfully is displayed with green color.

You can set the runner to always have this output, by default, if you set the appropriate configuration line inside the .rspec file in the root folder of your project. Just add this line --format doc:
--require spec_helper
--format doc

And then try to run again your specs by simply giving bundle exec rspec. You will see that the output is using the documentation formatter.

Let’s Write a Real Specification

Now that you have set the scene to be able to run your specs, let’s write your first real specification for the sandwich model. Remember that you are doing TDD/BDD. So you first write the specs and then the implementation in the core code that satisfies the specs.

The following is the new version of the file spec/sandwich_spec.rb (Listing 4-5).
# File: spec/sandwich_spec.rb
#
RSpec.describe 'An ideal sandwich' do
  it 'is delicious' do
    sandwich = Sandwich.new('delicious', [])
    taste = sandwich.taste
    expect(taste).to eq('delicious')
  end
end
Listing 4-5

First Real Specification

Usually, the tests are logically divided into four phases:
  1. 1.

    The setup phase.

     
  2. 2.

    The fire phase.

     
  3. 3.

    The assertion phase.

     
  4. 4.

    The teardown phase.

     
You may find these phases called with other names too:
  1. 1.

    Setup

     
  2. 2.

    Exercise

     
  3. 3.

    Verify

     
  4. 4.

    Teardown

     
However, usually, you find only three phases, as you have in the preceding example. See Figure 4-7.
../images/497830_1_En_4_Chapter/497830_1_En_4_Fig7_HTML.jpg
Figure 4-7

Three Phases of Test Code

Keep this in your mind whenever you design a test. You need to make sure you understand where is the setup code, where is the fire code, and where is the assertion code. If this is not clear from your code, then you need to make it be. Tests need to be simple and easy to identify those phases.

However, identification of those phases is not very straightforward, at all times. For example, you might mix the fire and the assertion together in order to avoid using temporary variables. See the new version of the spec/sandwich_spec.rb file (Listing 4-6).
# File: spec/sandwich_spec.rb
#
RSpec.describe 'An ideal sandwich' do
  it 'is delicious' do
    sandwich = Sandwich.new('delicious', [])
    expect(sandwich.taste).to eq('delicious')
  end
end
Listing 4-6

Mixed Fire and Assertion Phase

You can now see that the fire phase has been mixed with the assertion phase. But, even so, the test is still clear and the reader can understand that the part inside the expect(....) parentheses is the fire part (Figure 4-8).
../images/497830_1_En_4_Chapter/497830_1_En_4_Fig8_HTML.jpg
Figure 4-8

Fire Phase Mixed with Assertion Phase

Before you give a go to run the preceding spec, let’s also tell something about the expect method. The expect method call takes as argument the actual value. Then you can call an expectation target. In your case, this is .to. The expectation target then takes as an argument an RSpec matcher. In your example, this is the eq() method call. I will talk about all these in more detail later on. But, until then, you can read the expect(<actual>).to eq(<expected>) as we expect the value <actual> to be equal to the value <expected>. If the expectation fails, then expect raises the exception RSpec tools::Expectations::ExpectationNotMetError and terminates the example execution considering that as failed (Figure 4-9).

../images/497830_1_En_4_Chapter/497830_1_En_4_Fig9_HTML.jpg
Figure 4-9

RSpec Expectation Not Met Error

Sidenote

Since the RSpec tools::Expectations::ExpectationNotMetError superclass is Exception, this means that a rescue without specifying this particular class will not rescue these types of exceptions. Remember that the default for rescue is StandardError.

Run and Watch That Fail and Then Fix It

I will keep on saying that you are doing TDD (Test-Driven Development). So you first need to see your tests failing. Let’s run rspec to see that:
$ bundle exec rspec
Randomized with seed 43204
An ideal sandwich
  is delicious (FAILED - 1)
Failures:
  1) An ideal sandwich is delicious
     Failure/Error: sandwich = Sandwich.new('delicious', [])
     NameError:
       uninitialized constant Sandwich
     # ./spec/sandwich_spec.rb:5:in `block (2 levels) in <top (required)>'
Finished in 0.00236 seconds (files took 0.21178 seconds to load)
1 example, 1 failure
Failed examples:
rspec ./spec/sandwich_spec.rb:4 # An ideal sandwich is delicious
Randomized with seed 43204
$
The failure that you get is uninitialized constant Sandwich. Fair enough. Let’s create this class. In the root folder of your project, create the file sandwich.rb and put the following content inside (Listing 4-7).
# File: sandwich.rb
#
class Sandwich
end
Listing 4-7

sandwich.rb First Version

Also, you create the file all.rb that will require all the necessary files of your application (Listing 4-8).
# File: all.rb
#
require_relative 'sandwich'
Listing 4-8

all.rb Requires All Files

Then, you tell the spec/spec_helper.rb to load your application before exercising any example. Add the following line at the bottom of your spec/spec_helper.rb file:
require_relative '../all'
Then run the bundle exec rspec command again:
$ bundle exec rspec
Randomized with seed 64232
An ideal sandwich
  is delicious (FAILED - 1)
Failures:
  1) An ideal sandwich is delicious
     Failure/Error: sandwich = Sandwich.new('delicious', [])
     ArgumentError:
       wrong number of arguments (given 2, expected 0)
     # ./spec/sandwich_spec.rb:5:in `initialize'
     # ./spec/sandwich_spec.rb:5:in `new'
     # ./spec/sandwich_spec.rb:5:in `block (2 levels) in <top (required)>'
Finished in 0.00257 seconds (files took 0.19019 seconds to load)
1 example, 1 failure
Failed examples:
rspec ./spec/sandwich_spec.rb:4 # An ideal sandwich is delicious
Randomized with seed 64232
$
You’ve got rid of the uninitialized constant error. Now you have an ArgumentError. This has to do with the call to sandwich = Sandwich.new('delicious', []). This is expected. Your Sandwich class does not have an initializer that could take two arguments. Let’s correct that (Listing 4-9).
# File: sandwich.rb
#
class Sandwich
  def initialize(taste, toppings)
    @taste = taste
    @toppings = toppings
  end
end
Listing 4-9

sandwich.rb File with the Correct Initializer

Now, you run the spec again:
$ bundle exec rspec
Randomized with seed 12272
An ideal sandwich
  is delicious (FAILED - 1)
Failures:
  1) An ideal sandwich is delicious
     Failure/Error: expect(sandwich.taste).to eq('delicious')
     NoMethodError:
       undefined method `taste' for #<Sandwich:0x00007fc39518e358 @taste="delicious", @toppings=[]>
     # ./spec/sandwich_spec.rb:7:in `block (2 levels) in <top (required)>'
Finished in 0.00223 seconds (files took 0.17754 seconds to load)
1 example, 1 failure
Failed examples:
rspec ./spec/sandwich_spec.rb:4 # An ideal sandwich is delicious
Randomized with seed 12272
$
The runner is complaining that your Sandwich instance does not have a method taste. Let’s add that (Listing 4-10).
# File: sandwich.rb
#
class Sandwich
  def initialize(taste, toppings)
    @taste = taste
    @toppings = toppings
  end
  def taste
  end
end
Listing 4-10

taste Method Added

And let’s run again:
$ bundle exec rspec
Randomized with seed 65088
An ideal sandwich
  is delicious (FAILED - 1)
Failures:
  1) An ideal sandwich is delicious
     Failure/Error: expect(sandwich.taste).to eq('delicious')
       expected: "delicious"
            got: nil
       (compared using ==)
     # ./spec/sandwich_spec.rb:7:in `block (2 levels) in <top (required)>'
Finished in 0.0376 seconds (files took 0.1894 seconds to load)
1 example, 1 failure
Failed examples:
rspec ./spec/sandwich_spec.rb:4 # An ideal sandwich is delicious
Randomized with seed 65088
$
Now you have an expectation failure. The spec on line 7 expected the actual to be "delicious", but it is nil. Line 7 of the spec is
expect(sandwich.taste).to eq('delicious')
In other words, the sandwich.taste should return "delicious", but it does not. Let’s fix that (Listing 4-11).
# File: sandwich.rb
#
class Sandwich
  def initialize(taste, toppings)
    @taste = taste
    @toppings = toppings
  end
  def taste
    @taste
  end
end
Listing 4-11

Fix taste Method Implementation

Let’s run the tests again:
$ bundle exec rspec
Randomized with seed 28975
An ideal sandwich
  is delicious
Finished in 0.00108 seconds (files took 0.11061 seconds to load)
1 example, 0 failures
Randomized with seed 28975
$
Bingo! You just finished, once more, a round of a TDD workflow:
  1. 1.

    Write the spec.

     
  2. 2.

    Run it to see it failing.

     
  3. 3.

    Fix it.

     
However, there is one more phase in the TDD workflow: the refactoring phase. Let’s do a small improvement on our Sandwich class (Listing 4-12).
# File: sandwich.rb
#
class Sandwich
  attr_reader :taste
  def initialize(taste, toppings)
    @taste = taste
    @toppings = toppings
  end
end
Listing 4-12

Refactoring the Sandwich Class

After the refactoring phase, you then run the specs again to make sure that you have not broken anything:
$ bundle exec rspec
Randomized with seed 31688
An ideal sandwich
  is delicious
Finished in 0.00328 seconds (files took 0.1915 seconds to load)
1 example, 0 failures
Randomized with seed 31688
$

Sharing Setup Code

You will now add one more example as part of your Sandwich specification (Listing 4-13).
# File: spec/sandwich_spec.rb
#
RSpec.describe 'An ideal sandwich' do
  it 'is delicious' do
    sandwich = Sandwich.new('delicious', [])
    expect(sandwich.taste).to eq('delicious')
  end
  it 'lets me add toppings' do
    sandwich = Sandwich.new('delicious', [])
    sandwich.toppings << 'cheese'
    expect(sandwich.toppings).not_to be_empty
  end
end
Listing 4-13

More Specs in Sandwich

You can see that between lines 10 and 16. The actual is sandwich.toppings, and the expectation is that it should not be empty.

Let’s run the specs:
$ bundle exec rspec
Randomized with seed 27729
An ideal sandwich
  is delicious
  lets me add toppings (FAILED - 1)
Failures:
  1) An ideal sandwich lets me add toppings
     Failure/Error: sandwich.toppings << 'cheese'
     NoMethodError:
       undefined method `toppings' for #<Sandwich:0x00007fc83583b300 @taste="delicious", @toppings=[]>
     # ./spec/sandwich_spec.rb:13:in `block (2 levels) in <top (required)>'
Finished in 0.00383 seconds (files took 0.21256 seconds to load)
2 examples, 1 failure
Failed examples:
rspec ./spec/sandwich_spec.rb:10 # An ideal sandwich lets me add toppings
Randomized with seed 27729
$
The first example was successful. The second failed. Following the TDD approach, you fill in the necessary bits of code inside the Sandwich class to make this second example succeed too (Listing 4-14).
# File: sandwich.rb
#
class Sandwich
  attr_reader :taste, :toppings
  def initialize(taste, toppings)
    @taste = taste
    @toppings = toppings
  end
end
Listing 4-14

Fill In Necessary Bits of Code to Make All Suite Green

If you run the specs again, you will see them succeed:
$ bundle exec rspec
Randomized with seed 45205
An ideal sandwich
  lets me add toppings
  is delicious
Finished in 0.01153 seconds (files took 0.2327 seconds to load)
2 examples, 0 failures
Randomized with seed 45205
$
However, there is a repetition in the example file (Figure 4-10).
../images/497830_1_En_4_Chapter/497830_1_En_4_Fig10_HTML.jpg
Figure 4-10

Setup Is Repeated

You can see that the setup code is repeated at the beginning of your two examples. If you had many more, that repetition would increase the cost of changing and maintaining the setup phase of these examples. Can you DRY (Do not Repeat Yourself) the example group code?

Yes, you can. RSpec offers various ways you can do this.

before Hook

One way you can put your common setup code into one place is to use the before hook . The before hook hosts a piece of code that will be executed before the execution of every example.

Let’s do that for the spec/sandwich_spec.rb file (Listing 4-15).
# File: spec/sandwich_spec.rb
#
RSpec.describe 'An ideal sandwich' do
  before do
    @sandwich = Sandwich.new('delicious', [])
  end
  it 'is delicious' do
    expect(@sandwich.taste).to eq('delicious')
  end
  it 'lets me add toppings' do
    @sandwich.toppings << 'cheese'
    expect(@sandwich.toppings).not_to be_empty
  end
end
Listing 4-15

Use of the before Block

Inside the body of the block that is attached to the RSpec.describe call, you use the method before with a block attached. This is the way you define code that you want to be executed every time an example starts. Hence
  1. 1.

    Before the example is delicious, the runner will instantiate the variable @sandwich with the value Sandwich.new('delicious', []).

     
  2. 2.

    Then it will execute the is delicious code.

     
  3. 3.

    Then it will clear all the memory that is related to the example that finished before starting the next example.

     
  4. 4.

    Before the example lets me add toppings, it will execute the code inside the before block. It will instantiate a new @sandwich instance.

     
  5. 5.

    Then it will execute the code for the example lets me add toppings.

     

And so on.

The technique used here, with the before hook, eliminates the need to have the setup code written multiple times. However, you need to turn your local sandwich variables to references to the instance variable @sandwich (Figure 4-11).
../images/497830_1_En_4_Chapter/497830_1_En_4_Fig11_HTML.jpg
Figure 4-11

@sandwich Used Instead of sandwich

The before hooks are great if you want to prepare the common setup of your examples. They are also very useful if this has to do with things that need to take place in a world outside of your application, for example, to clean up the database that your application might be accessing. You might want every example to run on a clean database.

However, when it comes to instantiating variables, the before hook is not very handy. The maintenance of the instance variable is error prone and involves lots of find/and/replace actions with your editor.

Helper Methods

Another technique that you can use are helper methods. The helper methods are methods defined inside the RSpec.describe do ... end block, that is, at the example group level. They can then be used by the do ... end blocks of your it method calls, that is, at the example level.

The following is the version of spec/sandwich_spec.rb that uses this technique (Listing 4-16).
# File: spec/sandwich_spec.rb
#
RSpec.describe 'An ideal sandwich' do
  def sandwich
    @sandwich ||= Sandwich.new('delicious', [])
  end
  it 'is delicious' do
    expect(sandwich.taste).to eq('delicious')
  end
  it 'lets me add toppings' do
    sandwich.toppings << 'cheese'
    expect(sandwich.toppings).not_to be_empty
  end
end
Listing 4-16

Using Helper Methods

You can now see that you went back using non-instance variables. The sandwich is now a call to a method that is defined inside the RSpec.describe do ... end block. Note how the method definition is using memoization. Only the first time that it is called it actually calls Sandwich.new('delicious', []). Every other call to the sandwich method returns the value of the instance variable @sandwich and does not instantiate a new Sandwich. For example, for the spec lets me add toppings, only line 13 instantiates a Sandwich. Line 15, which calls sandwich again, uses the value stored in @sandwich.

Save the preceding code and run the specs again. You will see that they will be green.

The preceding technique is useful, and you will find it in many specifications in many projects. However, the memoization is a little bit tricky when the actual value is falsey. If the actual value is falsey, then memoization like this does not really work.

The following irb Ruby code demonstrates the problem with memoization and falsey values:
$ irb
irb(main):001:0> def do_something
irb(main):002:1>   puts 'I am doing something'
irb(main):003:1>   false
irb(main):004:1> end
=> :do_something
irb(main):005:0> foo ||= do_something
I am doing something
=> false
irb(main):006:0> foo ||= do_something
I am doing something
=> false
irb(main):007:0>
As you can see, the second call to foo ||= do_something calls the method do_something and does not only evaluate the foo variable. On the other hand, if do_something returns true, the second call to foo ||= do_something will not evaluate the do_something and will return the value of foo. See how this latter case works in the following:
irb(main):007:0> def do_something
irb(main):008:1>   puts 'I am doing something'
irb(main):009:1>   true
irb(main):010:1> end
=> :do_something
irb(main):011:0> foo ||= do_something
I am doing something
=> true
irb(main):012:0> foo ||= do_something
=> true

Let’s see another technique that is very popular with RSpec and allows for sharing of code.

Share with let

Instead of defining helper methods, you can call the method let that evaluates a block of Ruby code only once, at the first time you use the name of the let . Here is the new version of spec/sandwich_spec.rb that is using let (Listing 4-17).
# File: spec/sandwich_spec.rb
#
RSpec.describe 'An ideal sandwich' do
  let(:sandwich) do
    Sandwich.new('delicious', [])
  end
  it 'is delicious' do
    expect(sandwich.taste).to eq('delicious')
  end
  it 'lets me add toppings' do
    sandwich.toppings << 'cheese'
    expect(sandwich.toppings).not_to be_empty
  end
end
Listing 4-17

Using let

The preceding let binds the block of code Sandwich.new('delicious', []) to the name sandwich. Then you can use the sandwich name in your examples. The sandwich will be evaluated only once, and subsequent calls will use the first-time evaluated value.

If you save and run your tests, you will see that everything is green again.

Context

RSpec uses one more method of grouping examples, tests. This is called context , and it is no different from RSpec.describe. Actually, it is an alias to RSpec.describe. The point is that with context, you make the grouping of the examples more intuitive since you group examples according to the context the examples assume they live in.

Let’s see that with an example. Write the file spec/coffee_spec.rb as follows (Listing 4-18).
# File: spec/coffee_spec.rb
#
RSpec.describe 'A coffee' do
  let(:coffee) { Coffee.new }
  it 'costs 1 euro' do
    expect(coffee.price).to eq(1)
  end
  context 'with milk' do
    before { coffee.add_milk }
    it 'costs 1.2 euro' do
      expect(coffee.price).to eq(1.2)
    end
  end
end
Listing 4-18

Using Context

Before I explain what is going on in this spec file, let’s run it without actually evaluating the examples. You will do what is called a dry run in order to see the output of the example definitions:
$ bundle exec rspec --dry-run
Randomized with seed 32424
A coffee
  costs 1 euro
  with milk
    costs 1.2 euro
An ideal sandwich
  is delicious
  lets me add toppings
Finished in 0.00288 seconds (files took 0.19913 seconds to load)
4 examples, 0 failures
Randomized with seed 32424
$
This is a nice formatted output of your specs without any evaluation. But it is displaying how you have structured your specs. See how the with milk context is creating a new indentation for the examples that it is grouping (Figure 4-12).
../images/497830_1_En_4_Chapter/497830_1_En_4_Fig12_HTML.jpg
Figure 4-12

context Creates a New Indentation

You can see how the context created a new block of indented documentation for the grouped examples.

Something else that you need to be aware of is that context can create before hooks as the describe does. And the before hooks are evaluated only for the examples that belong to the particular context.

Running Specific Examples

Now let’s try to run the suite of specs with a normal (not dry) run:
$ bundle exec rspec
Randomized with seed 64299
A coffee
  costs 1 euro (FAILED - 1)
  with milk
    costs 1.2 euro (FAILED - 2)
An ideal sandwich
  is delicious
  lets me add toppings
Failures:
  1) A coffee costs 1 euro
     Failure/Error: let(:coffee) { Coffee.new }
     NameError:
       uninitialized constant Coffee
     # ./spec/coffee_spec.rb:4:in `block (2 levels) in <top (required)>'
     # ./spec/coffee_spec.rb:7:in `block (2 levels) in <top (required)>'
  2) A coffee with milk costs 1.2 euro
     Failure/Error: let(:coffee) { Coffee.new }
     NameError:
       uninitialized constant Coffee
     # ./spec/coffee_spec.rb:4:in `block (2 levels) in <top (required)>'
     # ./spec/coffee_spec.rb:11:in `block (3 levels) in <top (required)>'
Finished in 0.00612 seconds (files took 0.21116 seconds to load)
4 examples, 2 failures
Failed examples:
rspec ./spec/coffee_spec.rb:6 # A coffee costs 1 euro
rspec ./spec/coffee_spec.rb:13 # A coffee with milk costs 1.2 euro
Randomized with seed 64299
$

You can see that you have two failures, both coming from the spec/coffee_spec.rb file.

You may have hundreds of spec files and only one failing. How can you run the rspec runner for that particular file only? Easy stuff: just give the filename as argument to RSpec tools:
$ bundle exec rspec spec/coffee_spec.rb
Randomized with seed 47485
A coffee
  costs 1 euro (FAILED - 1)
  with milk
    costs 1.2 euro (FAILED - 2)
Failures:
  1) A coffee costs 1 euro
     Failure/Error: let(:coffee) { Coffee.new }
     NameError:
       uninitialized constant Coffee
     # ./spec/coffee_spec.rb:4:in `block (2 levels) in <top (required)>'
     # ./spec/coffee_spec.rb:7:in `block (2 levels) in <top (required)>'
  2) A coffee with milk costs 1.2 euro
     Failure/Error: let(:coffee) { Coffee.new }
     NameError:
       uninitialized constant Coffee
     # ./spec/coffee_spec.rb:4:in `block (2 levels) in <top (required)>'
     # ./spec/coffee_spec.rb:11:in `block (3 levels) in <top (required)>'
Finished in 0.00302 seconds (files took 0.18583 seconds to load)
2 examples, 2 failures
Failed examples:
rspec ./spec/coffee_spec.rb:6 # A coffee costs 1 euro
rspec ./spec/coffee_spec.rb:13 # A coffee with milk costs 1.2 euro
Randomized with seed 47485
$

As you can see, you just run only the examples inside the file spec/coffee_spec.rb.

Let’s try to fix the first of the examples that is failing.

Create the file coffee.rb and add the class definition for Coffee, like the following (Listing 4-19).
# File: coffee.rb
#
class Coffee
  def price
    1
  end
end
Listing 4-19

First Version of the coffee.rb File

Make sure that the file all.rb requires the coffee.rb file too (Listing 4-20).
# File: all.rb
#
require_relative 'sandwich'
require_relative 'coffee'
Listing 4-20

all.rb Requires coffee.rb

Now, let’s run the specs for the particular file again:
$ bundle exec rspec spec/coffee_spec.rb
Randomized with seed 36624
A coffee
  costs 1 euro
  with milk
    costs 1.2 euro (FAILED - 1)
Failures:
  1) A coffee with milk costs 1.2 euro
     Failure/Error: before { coffee.add_milk }
     NoMethodError:
       undefined method `add_milk' for #<Coffee:0x00007fd9691b0f18>
     # ./spec/coffee_spec.rb:11:in `block (3 levels) in <top (required)>'
Finished in 0.0039 seconds (files took 0.20848 seconds to load)
2 examples, 1 failure
Failed examples:
rspec ./spec/coffee_spec.rb:13 # A coffee with milk costs 1.2 euro
Randomized with seed 36624
$

Nice! Only one failure, for the example A coffee with milk costs 1.2 euro.

Imagine that your spec/coffee_spec.rb file contains many more examples, but you only want to run the particular one that failed. How can you do that? You will do that by specifying both the filename and the line number corresponding to the failing example. Actually, the last rspec run gives you the command ready to copy and paste into your terminal (Figure 4-13).
../images/497830_1_En_4_Chapter/497830_1_En_4_Fig13_HTML.jpg
Figure 4-13

rspec Gives You the Command to Run a Specific Example

Let’s do that. Run the command for the specific failing example:
$ bundle exec rspec spec/coffee_spec.rb:13
Run options: include {:locations=>{"./spec/coffee_spec.rb"=>[13]}}
Randomized with seed 54173
A coffee
  with milk
    costs 1.2 euro (FAILED - 1)
Failures:
  1) A coffee with milk costs 1.2 euro
     Failure/Error: before { coffee.add_milk }
     NoMethodError:
       undefined method `add_milk' for #<Coffee:0x00007fc4968af3b0>
     # ./spec/coffee_spec.rb:11:in `block (3 levels) in <top (required)>'
Finished in 0.00136 seconds (files took 0.18474 seconds to load)
1 example, 1 failure
Failed examples:
rspec ./spec/coffee_spec.rb:13 # A coffee with milk costs 1.2 euro
Randomized with seed 54173
$
In order to finish with this example, let’s fix the code for the Coffee class to make this example go green. The following is the new version of the coffee.rb file (Listing 4-21).
# File: coffee.rb
#
class Coffee
  INGREDIENT_PRICES = {
      'milk' => 0.2
  }
  def initialize
    @ingredients = []
  end
  def price
    1 + sum_of_ingredients_price
  end
  def add_milk
    @ingredients << 'milk'
  end
  private
  def sum_of_ingredients_price
    @ingredients.reduce(0) do |result, ingredient|
      result += result + INGREDIENT_PRICES[ingredient]
    end
  end
end
Listing 4-21

New Version of coffee.rb

If you now run the command for the example that was failing before, you will see that it is green:
$ bundle exec rspec spec/coffee_spec.rb:13
Run options: include {:locations=>{"./spec/coffee_spec.rb"=>[13]}}
Randomized with seed 39570
A coffee
  with milk
    costs 1.2 euro
Finished in 0.00213 seconds (files took 0.1954 seconds to load)
1 example, 0 failures
Randomized with seed 39570
$
Having fixed one file with specs, you now go ahead and run the whole suite again:
$ bundle exec rspec
Randomized with seed 15573
A coffee
  costs 1 euro
  with milk
    costs 1.2 euro
An ideal sandwich
  is delicious
  lets me add toppings
Finished in 0.00668 seconds (files took 0.1776 seconds to load)
4 examples, 0 failures
Randomized with seed 15573
$

Everything is green. If you want, be happy to refactor the implementation. If refactoring breaks any of the application specifications, you will know that by running the whole test suite again.

RubyMine Integration

Before you continue with more advanced topics on Rspec, let’s make sure that you can run your RSpec suite from within RubyMine.

Right-click your project name, in the RubyMine project explorer, and then select to run all specs in your project, as in the following screenshot (Figure 4-14).
../images/497830_1_En_4_Chapter/497830_1_En_4_Fig14_HTML.jpg
Figure 4-14

RubyMine – Run All Specs

When you do that, RubyMine will execute all the specs in your project (Figure 4-15).
../images/497830_1_En_4_Chapter/497830_1_En_4_Fig15_HTML.png
Figure 4-15

RubyMine – Run All Specs Results

You can even run an individual file. Just right-click and select to run the particular file, as in Figure 4-16 for the sandwich_spec.rb.
../images/497830_1_En_4_Chapter/497830_1_En_4_Fig16_HTML.jpg
Figure 4-16

RubyMine – Run a Specific Spec File

The result will appear again at the bottom tab of RubyMine.

And, finally, you can run an individual example or example group. In the following screenshot, I show how I right-click inside the example lets me add toppings and then I select to run the particular example (Figure 4-17).
../images/497830_1_En_4_Chapter/497830_1_En_4_Fig17_HTML.jpg
Figure 4-17

RubyMine – Run a Specific Example

Note that every time you run your specs through RubyMine, it creates a run configuration, and you can re-execute it with the click of a button (Figure 4-18).
../images/497830_1_En_4_Chapter/497830_1_En_4_Fig18_HTML.jpg
Figure 4-18

How to Run the Last Spec Again

Before I close these notes about the integration with RubyMine, I would like to make you aware of the keyboard shortcuts that can be used to execute the whole suite or a specific example. For example, on my Mac, when the cursor is inside an example, then I can press the keyboard combination Ctrl+Shift+R, and it will run the rspec for the particular example (or context/describe if my cursor is in a context/describe).

Task Details

WRITE TESTS USING RSPEC
You are requested to implement a class that will satisfy the following requirements:
A Mad Libs Story Teller
  when it gets the story template 'A ((an animal)) in the ((a body part)) is worth ((a number)) in the ((a place))'
    then its third question to ask is 'Give me a number'
    then its fourth question to ask is 'Give me a place'
    then its first question to ask is 'Give me an animal'
    then its second question to ask is 'Give me a body part'
    and words for placeholders 'cat', 'hand', 'twelve' and 'London'
      produces the story 'A cat in the hand is worth twelve in the London'
  when it gets the story template 'I had a ((an adjective)) sandwich for lunch today. It dripped all over my ((a body part)) and ((a noun))'
    then its second question to ask is 'Give me a body part'
    then its third question to ask is 'Give me a noun'
    then its first question to ask is 'Give me an adjective'
    and words for placeholders 'smelly', 'big toe' and 'bathtub'
      produces the story 'I had a smelly sandwich for lunch today. It dripped all over my big toe and bathtub'
Finished in 0.00213 seconds (files took 0.07707 seconds to load)
9 examples, 0 failures
This is the screenshot of the output of the RSpec documentation that might be easier for you to read (Figure 4-19).
../images/497830_1_En_4_Chapter/497830_1_En_4_Fig19_HTML.jpg
Figure 4-19

Task – RSpec Formatted Documentation

This exercise is based on the following business requirements:

There is a game called Mad Libs. This is based on a story template. For example, “I ((a verb)) to eat ((a food)).” This story template has two placeholders: the “((a verb))” and the “((a food)).” The game is played with one person holding the story template hidden from the others. But this person asks them questions based on the placeholder content. For this example, the first question is “Give me a verb.” And the second question is “Give me a food.” The other person answers, and the person who holds the story template compiles the story based on these answers. Hence, if the person who answers gives the answers “run” and “pizza,” then the story that is compiled is “I run to eat pizza.”

What you have to implement is not the whole interaction of the game, just the class that has the business rules the game relies on.

In fact, you need to implement that behavior that is documented by the RSpec documentation output that you see in the preceding figure. You have to implement both the class that satisfies these requirements and the RSpec specs that describe them.

Key Takeaways

  • How to install and initialize RSpec

  • How to configure RSpec

  • How to write specs in a specific folder and naming convention

  • How to output results in a documentation format

  • How to share the setup code between specs

  • How to write specs inside a context

RSpec is very powerful. In the next chapter, you are going to learn what extra tools it offers.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.134.79.121