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

5. Useful RSpec Tools

Panos Matsinopoulos1 
(1)
KERATEA, Greece
 
RSpec is a very feature-rich library. In this chapter, you learn about the most frequently used features (Figure 5-1), and you try to apply them to a real Ruby application. Also, you will be requested to develop a Ruby class and test cover it with RSpec.
../images/497830_1_En_5_Chapter/497830_1_En_5_Fig1_HTML.jpg
Figure 5-1

Useful RSpec Tools

Learning Goals

  1. 1.

    Learn about RSpec before and after hooks.

     
  2. 2.

    Learn about the subject reference.

     
  3. 3.

    Learn about named subjects.

     
  4. 4.

    Learn about how to extend RSpec with helper methods in modules.

     
  5. 5.

    Learn about RSpec metadata.

     
  6. 6.

    Learn about filtering.

     

Hooks

You have already learned about the before hook that RSpec offers and allows you to prepare the context, that is, the setup of your test, of your example. However, RSpec offers some extra hooks.

before(:suite)

This is a hook for things you want to execute at the start of the rspec runner. Let’s see that with an example. Go to the project that you have created in the previous chapter (project: coffee_shop) and open the file spec/sandwich_spec.rb. At the beginning of the file, write the following piece of code (Listing 5-1).
RSpec.configure do |config|
  config.before :suite do
    puts 'This is executed only once at the beginning of the specifications run'
  end
end
Listing 5-1

At the Beginning of spec/sandwich_spec.rb

Hence, the spec/sandwich_spec.rb now looks like the following (Listing 5-2).
# File: spec/sandwich_spec.rb
#
RSpec.configure do |config|
  config.before :suite do
    puts 'This is executed only once at the beginning of the specifications run'
  end
end
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 5-2

Full Version of spec/sandwich_spec.rb

The before :suite, since it is not related to any specific example or example group, can only be registered from within an RSpec configuration block. This is what the preceding lines 3–7 do. You start an RSpec.configuration block, and you call config.before :suite do ... end in order to register code that will be executed at the start of the spec runner.

If you run your test suite, you will see this:
$ bundle exec rspec
Randomized with seed 1770
This is executed only once at the beginning of the specifications run
An ideal sandwich
  is delicious
  lets me add toppings
A coffee
  costs 1 euro
  with milk
    costs 1.2 euro
Finished in 0.00873 seconds (files took 0.24097 seconds to load)
4 examples, 0 failures
Randomized with seed 1770
$

You can see the message This is executed only once at the beginning of the specifications run that is being printed at the beginning (after the Randomized with seed 1770).

The before :suite hook is usually used to set the correct test execution context for all the tests in your suite, for example, to load your test database with some seed data.

Moreover, because of the fact that it is executed before the instantiation of the examples, you cannot instantiate variables and expect to have them available inside the blocks of code of your examples.

Let’s see an example of this error. In Listing 5-3, I enhance the spec/sandwich.rb file to use the instance variable @foo, which is instantiated inside the before :suite hook.
# File: spec/sandwich_spec.rb
#
RSpec.configure do |config|
  config.before :suite do
    puts 'This is executed only once at the beginning of the specifications run'
    @foo = 5
  end
end
RSpec.describe 'An ideal sandwich' do
  def sandwich
    @sandwich ||= Sandwich.new('delicious', [])
  end
  it 'is delicious' do
    expect(@foo).to eq(5)
    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 5-3

Use the Instance Variable @foo

Inside the test is delicious, I have written an expectation to check for the value of @foo being 5. If you run the specs, you will see that they will fail:
$ bundle exec rspec
Randomized with seed 27481
This is executed only once at the beginning of the specifications run
A coffee
  costs 1 euro
  with milk
    costs 1.2 euro
An ideal sandwich
  is delicious (FAILED - 1)
  lets me add toppings
Failures:
  1) An ideal sandwich is delicious
     Failure/Error: expect(@foo).to eq(5)
       expected: 5
            got: nil
       (compared using ==)
     # ./spec/sandwich_spec.rb:16:in `block (2 levels) in <top (required)>'
Finished in 0.0566 seconds (files took 0.20695 seconds to load)
4 examples, 1 failure
Failed examples:
rspec ./spec/sandwich_spec.rb:15 # An ideal sandwich is delicious
Randomized with seed 27481
$
Finally, you usually put the before(:suite) hooks inside the spec_helper.rb file. So, before you continue with the rest of this chapter, move that code at the bottom of the existing RSpec.configure block inside the spec/spec_helper.rb file (Listing 5-4).
# 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
  config.before :suite do
    puts 'This is executed only once at the beginning of the specifications run'
  end
end
require_relative '../all'
Listing 5-4

spec/spec_helper.rb File with the before(:suite) Hook

After you do the preceding change, don’t forget to bring spec/spec_helper.rb back to its original state: without the RSpec.configure block and without the expectation for @foo to be 5 inside the is delicious example.

Then, make sure that if you run the whole suite, everything is still green.

before(:all)

This hook attaches code that is executed once for all the examples on a given context/describe. So this should appear inside the do...end block of a describe or context.

In order to see an example of this, you are going to enhance the spec/coffee_spec.rb specifications as follows (Listing 5-5).
# 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
  it 'takes 1 minute to prepare' do
    expect(coffee.prepare_duration).to eq(1)
  end
  context 'with milk' do
    before :all do
      puts 'This is a hook that will be executed once for the "with milk" context examples'
      @with_milk = 'Milk has been added!'
    end
    before { coffee.add_milk }
    it 'costs 1.2 euro' do
      puts @with_milk
      expect(coffee.price).to eq(1.2)
    end
    it 'takes 2 minutes to prepare' do
      puts @with_milk
      expect(coffee.prepare_duration).to eq(2)
    end
  end
end
Listing 5-5

spec/coffee_spec.rb File with the before :all Hook

These are the changes to the code, as in Figure 5-2:
../images/497830_1_En_5_Chapter/497830_1_En_5_Fig2_HTML.jpg
Figure 5-2

Changes to Code to Demonstrate the before :all Hook

  1. 1.

    You have added two more expectations that have to do with the duration to prepare a cup of coffee.

     
  2. 2.

    You have added a before(:all) hook on the context with milk. This code will be executed only once for the two examples in that context.

     
Let’s run the suite:
$ bundle exec rspec
Randomized with seed 16834
This is executed only once at the beginning of the specifications run
A coffee
  takes 1 minute to prepare (FAILED - 1)
  costs 1 euro
  with milk
This is a hook that will be executed once for the "with milk" context examples
Milk has been added!
    costs 1.2 euro
Milk has been added!
    takes 2 minutes to prepare (FAILED - 2)
An ideal sandwich
  is delicious
  lets me add toppings
Failures:
  1) A coffee takes 1 minute to prepare
     Failure/Error: expect(coffee.prepare_duration).to eq(1)
     NoMethodError:
       undefined method `prepare_duration' for #<Coffee:0x00007f936b09a500 @ingredients=[]>
     # ./spec/coffee_spec.rb:11:in `block (2 levels) in <top (required)>'
  2) A coffee with milk takes 2 minutes to prepare
     Failure/Error: expect(coffee.prepare_duration).to eq(2)
     NoMethodError:
       undefined method `prepare_duration' for #<Coffee:0x00007f936a8f6010 @ingredients=["milk"]>
     # ./spec/coffee_spec.rb:29:in `block (3 levels) in <top (required)>'
Finished in 0.00819 seconds (files took 0.26973 seconds to load)
6 examples, 2 failures
Failed examples:
rspec ./spec/coffee_spec.rb:10 # A coffee takes 1 minute to prepare
rspec ./spec/coffee_spec.rb:27 # A coffee with milk takes 2 minutes to prepare
Randomized with seed 16834
$
As expected, the two examples that deal with the preparation duration fail. Before you fix them, see the messages that prove how the before(:all) hook works. It is called once for the whole context, and the instance variables created within its block are available to the code of the examples (Figure 5-3).
../images/497830_1_En_5_Chapter/497830_1_En_5_Fig3_HTML.jpg
Figure 5-3

Output of the RSpec Run Proves How the before(:all) Hook Works

Let’s quickly fix the code by introducing the #prepare_duration method in the Coffee class (file: coffee.rb – Listing 5-6).
# 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
  def prepare_duration
    1 + @ingredients.size
  end
  private
  def sum_of_ingredients_price
    @ingredients.reduce(0) do |result, ingredient|
      result += result + INGREDIENT_PRICES[ingredient]
    end
  end
end
Listing 5-6

Introduce Method prepare_duration

The new lines added are lines 20–22. Now, if you run the suite, you will get everything green, like this:
$ bundle exec rspec
Randomized with seed 8716
This is executed only once at the beginning of the specifications run
An ideal sandwich
  lets me add toppings
  is delicious
A coffee
  costs 1 euro
  takes 1 minute to prepare
  with milk
This is a hook that will be executed once for the "with milk" context examples
Milk has been added!
    takes 2 minutes to prepare
Milk has been added!
    costs 1.2 euro
Finished in 0.01081 seconds (files took 0.24569 seconds to load)
6 examples, 0 failures
Randomized with seed 8716
$
Note

before(:all) is also available as before(:context).

before(:example) or Simply before

You have already seen this hook. It is used to attach code that will be executed before every example in the context/describe it is declared in.

This is what happens on line 20 before { coffee.add_milk } inside the file spec/coffee_spec.rb.

Note that before(:example) is also available as before(:each).

after Hooks

Before I close this part about hooks, I need to tell you that, besides the before hooks, RSpec allows you to attach after hooks too. Here is the list of after hooks:
  1. 1.

    after :example, which is executed after every example. Also available as after :each or simply after

     
  2. 2.

    after :all, which is executed after every context. Also available as after :context

     
  3. 3.

    after :suite, which is executed at the end of the execution of the whole suite of examples

     

Multiple Hooks

Note that you can attach multiple hooks. They will be executed in the order they are declared.

subject

Let’s now talk about another technique and best practice that you will frequently encounter. It is about the subject under test.

You will many times see the argument to the outermost RSpec.describe being a class, rather than a string:
RSpec.describe Foo do
...
end

In that case, you can use the method subject inside your example code. Then the subject is going to be an instance of that class, instantiated with the default initializer.

Let’s see that with the spec/coffee_spec.rb. Instead of RSpec.describe 'A coffee' do...end, you use the class Coffee (Listing 5-7).
# File: spec/coffee_spec.rb
#
RSpec.describe Coffee do
  let(:coffee) { Coffee.new }
  it 'costs 1 euro' do
    expect(coffee.price).to eq(1)
  end
  it 'takes 1 minute to prepare' do
    expect(coffee.prepare_duration).to eq(1)
  end
  context 'with milk' do
    before :all do
      puts 'This is a hook that will be executed once for the "with milk" context examples'
      @with_milk = 'Milk has been added!'
    end
    before { coffee.add_milk }
    it 'costs 1.2 euro' do
      puts @with_milk
      expect(coffee.price).to eq(1.2)
    end
    it 'takes 2 minutes to prepare' do
      puts @with_milk
      expect(coffee.prepare_duration).to eq(2)
    end
  end
end
Listing 5-7

Use the Coffee class

If you run the specifications for Coffee , you will see this:
$ bundle exec rspec spec/coffee_spec.rb
Randomized with seed 33151
This is executed only once at the beginning of the specifications run
Coffee
  costs 1 euro
  takes 1 minute to prepare
  with milk
This is a hook that will be executed once for the "with milk" context examples
Milk has been added!
    costs 1.2 euro
Milk has been added!
    takes 2 minutes to prepare
Finished in 0.00505 seconds (files took 0.25892 seconds to load)
4 examples, 0 failures
Randomized with seed 33151
$

Now the Coffee string is printed as the outermost describe description.

Using the class is helping the reader bind the spec to an implementation, especially when the specs have to do with the unit tests of a class. But as I said earlier, you can use the subject to refer to the instance under test. Let’s adapt the spec/coffee_spec.rb to do that (Listing 5-8).
# File: spec/coffee_spec.rb
#
RSpec.describe Coffee do
  it 'costs 1 euro' do
    expect(subject.price).to eq(1)
  end
  it 'takes 1 minute to prepare' do
    expect(subject.prepare_duration).to eq(1)
  end
  context 'with milk' do
    before :all do
      puts 'This is a hook that will be executed once for the "with milk" context examples'
      @with_milk = 'Milk has been added!'
    end
    before { subject.add_milk }
    it 'costs 1.2 euro' do
      puts @with_milk
      expect(subject.price).to eq(1.2)
    end
    it 'takes 2 minutes to prepare' do
      puts @with_milk
      expect(subject.prepare_duration).to eq(2)
    end
  end
end
Listing 5-8

Use subject

You have removed the let(:coffee)... and all the coffee references. You are now using the subject instead.

If you run your specs for the Coffee, you will see that they will succeed.

So subject saves a little bit of typing, but the problem is that it hides the semantic meaning of the subject under test and the firing phase code is not really easy to understand quickly what it is about. In other words, the coffee.prepare_duration is much more descriptive and friendly to the reader rather than the subject.prepare_duration.

To alleviate that, some developers, including myself, still use the let; but they invoke the subject inside the let block: let(:coffee) { subject }. So here is my preferred version for the spec/coffee_spec.rb (Listing 5-9).
# File: spec/coffee_spec.rb
#
RSpec.describe Coffee do
  let(:coffee) { subject }
  it 'costs 1 euro' do
    expect(coffee.price).to eq(1)
  end
  it 'takes 1 minute to prepare' do
    expect(coffee.prepare_duration).to eq(1)
  end
  context 'with milk' do
    before :all do
      puts 'This is a hook that will be executed once for the "with milk" context examples'
      @with_milk = 'Milk has been added!'
    end
    before { coffee.add_milk }
    it 'costs 1.2 euro' do
      puts @with_milk
      expect(coffee.price).to eq(1.2)
    end
    it 'takes 2 minutes to prepare' do
      puts @with_milk
      expect(coffee.prepare_duration).to eq(2)
    end
  end
end
Listing 5-9

Using let and subject

This version goes back to be using coffee instead of subject inside the example code. But the coffee is defined in terms of the subject.

However, there is always the case in which the subject cannot be instantiated by the standard no-arguments initializer. See, for example, the case of the Sandwich instances. If you decide to give the RSpec.describe the Sandwich argument (instead of the An ideal sandwich), then subject will fail. Let’s try the version of spec/sandwich_spec.rb that uses subject (Listing 5-10).
# File: spec/sandwich_spec.rb
#
RSpec.describe Sandwich do
  let(:sandwich) do
    subject
  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 5-10

spec/sandwich_spec.rb Using subject

You can see that I have done two changes in order to introduce subject:
  1. 1.

    On line 3, I use RSpec.describe Sandwich do.

     
  2. 2.

    On lines 4–6, I define sandwich as a let of the subject.

     
But, if we run the specs for this file, they will fail:
$ bundle exec rspec spec/sandwich_spec.rb
Randomized with seed 30666
This is executed only once at the beginning of the specifications run
Sandwich
  lets me add toppings (FAILED - 1)
  is delicious (FAILED - 2)
Failures:
  1) Sandwich lets me add toppings
     Failure/Error: subject
     ArgumentError:
       wrong number of arguments (given 0, expected 2)
     # ./sandwich.rb:6:in `initialize'
     # ./spec/sandwich_spec.rb:5:in `block (2 levels) in <top (required)>'
     # ./spec/sandwich_spec.rb:13:in `block (2 levels) in <top (required)>'
  2) Sandwich is delicious
     Failure/Error: subject
     ArgumentError:
       wrong number of arguments (given 0, expected 2)
     # ./sandwich.rb:6:in `initialize'
     # ./spec/sandwich_spec.rb:5:in `block (2 levels) in <top (required)>'
     # ./spec/sandwich_spec.rb:9:in `block (2 levels) in <top (required)>'
Finished in 0.00312 seconds (files took 0.26276 seconds to load)
2 examples, 2 failures
Failed examples:
rspec ./spec/sandwich_spec.rb:12 # Sandwich lets me add toppings
rspec ./spec/sandwich_spec.rb:8 # Sandwich is delicious
Randomized with seed 30666
$
You can see that the error is the same for all the failing examples:
ArgumentError:
       wrong number of arguments (given 0, expected 2)
     # ./sandwich.rb:6:in `initialize'
     # ./spec/sandwich_spec.rb:5:in `block (2 levels) in <top (required)>'
     # ./spec/sandwich_spec.rb:13:in `block (2 levels) in <top (required)>'

On line spec/sandwich_spec.rb:5, you are calling subject which internally calls the Sandwich initializer without any arguments. That’s why you are getting ArgumentError and (0 for 2), that is, the initializer has been called with no arguments when two were expected.

How can you remedy this and still use subject? You need to redefine subject as follows (Listing 5-11).
# File: spec/sandwich_spec.rb
#
RSpec.describe Sandwich do
  subject(: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 5-11

Redefine subject

You create a named subject, you give it the name sandwich, and then you can use it in your example code.

One might ask, “Why would you want to do that and not just use let?” The answer is that I prefer to use subject to indicate the subject under test. I use let for other helper methods that I might need for my tests.

Note that subject is being memoized and evaluated only once within the execution of an example. So, even if your example calls the named subject multiple times, only the first time is being evaluated, as the let does.

Finally, both subject and let have the alternative subject! and let!, respectively. The subject! and let! evaluate their blocks before the example execution (whereas the subject and let are lazily evaluated at the first occurrence within the example code).

Extending RSpec with Helper Methods in Modules

You have learned how to create helper methods inside an example group. It has been presented in the previous chapter. Those methods were available inside the context in which they were defined, but not outside of it.

RSpec offers you the ability to define helper methods that can be used by any context, since they are attached at the RSpec configuration level.

How do you do that?

First, you need to define a module with the helper methods. Let’s see an example. Create the file upcased_and_line_spaced.rb in the root folder of your project (Listing 5-12).
# File: upcased_and_line_spaced.rb
#
module UpcasedAndLineSpaced
  # Takes "foo" and returns "F O O"
  #
  def upcased_and_line_spaced(value)
    value.upcase.gsub(/./) {|c| "#{c} "}.strip
  end
end
Listing 5-12

Example Module with a Helper Method

This module defines a helper method that takes a string value and returns it with all characters upcased. Also, it adds an extra space after each character (except the last one).

In order for you to be able to use this helper method, you need to attach the module to the RSpec configuration as follows:

The following snippet of code needs to be added to the bottom of the existing RSpec.configure block, inside the spec/spec_helper.rb file:
config.include UpcasedAndLineSpaced

And in order for the UpcasedAndLineSpaced constant to be found, you will have to require the module file before the RSpec.configure block.

Hence, the full content of the spec/spec_helper.rb file should be as shown in Listing 5-13.
# 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
require_relative '../upcased_and_line_spaced'
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
  config.before :suite do
    puts 'This is executed only once at the beginning of the specifications run'
  end
  config.include UpcasedAndLineSpaced
end
require_relative '../all'
Listing 5-13

spec/spec_helper.rb

Now, you can use the helper method inside the example code. Here is the new version of spec/sandwich_spec.rb (Listing 5-14).
# File: spec/sandwich_spec.rb
#
RSpec.describe Sandwich do
  subject(:sandwich) do
    Sandwich.new('delicious', [])
  end
  it 'is delicious' do
    expect(sandwich.taste).to eq('delicious')
  end
  it 'is displayed as D E L I C I O U S' do
    expect(upcased_and_line_spaced(sandwich.taste)).to eq('D E L I C I O U S')
  end
  it 'lets me add toppings' do
    sandwich.toppings << 'cheese'
    expect(sandwich.toppings).not_to be_empty
  end
end
Listing 5-14

Using the Helper Method from the Module

You have added a new specification. See lines 12–14. It is there where you call the upcased_and_line_spaced helper method.

If you run the specs now, you will get this:
$ bundle exec rspec spec/sandwich_spec.rb
Randomized with seed 5962
This is executed only once at the beginning of the specifications run
Sandwich
  is delicious
  lets me add toppings
  is displayed as D E L I C I O U S
Finished in 0.00902 seconds (files took 0.23133 seconds to load)
3 examples, 0 failures
Randomized with seed 5962
$

This is a very common practice with RSpec. You will encounter it in many projects. Note that when you do config.include <module>, the methods become available inside the example code. However, when you do config.extend <module>, the methods become available inside the do..end blocks of the describes and contexts.

Metadata

RSpec builds a set of metadata around your example groups and examples. Also, it allows you to build your custom metadata.

described_class

The call to described_class returns the class that is described when the outermost describe is given a class (and not a string description). You usually use it in order to avoid repeating the class name, hence being easier to change it, if you need to.

Let’s change the spec/sandwich_spec.rb to be using the described_class. Here it is (Listing 5-15).
# File: spec/sandwich_spec.rb
#
RSpec.describe Sandwich do
  subject(:sandwich) do
    described_class.new('delicious', [])
  end
  it 'is delicious' do
    expect(sandwich.taste).to eq('delicious')
  end
  it 'is displayed as D E L I C I O U S' do
    expect(upcased_and_line_spaced(sandwich.taste)).to eq('D E L I C I O U S')
  end
  it 'lets me add toppings' do
    sandwich.toppings << 'cheese'
    expect(sandwich.toppings).not_to be_empty
  end
end
Listing 5-15

Using described_class

The only change is on line 5. Instead of Sandwich.new, you are using described_class.new. Again, this allows you to change the class at the top call to described without having to change its other occurrences inside the spec file.

Custom Metadata

RSpec allows you to attach metadata to example groups or examples. The metadata are given in the form of a Hash as the last argument to describe, context, or it, before the block definition.

Here is an example: You are going to define some metadata in the spec/sandwich_spec.rb file (Listing 5-16).
# File: spec/sandwich_spec.rb
#
RSpec.describe Sandwich, test: :unit do
  subject(:sandwich) do
    described_class.new('delicious', [])
  end
  it 'is delicious' do
    expect(sandwich.taste).to eq('delicious')
  end
  it 'is displayed as D E L I C I O U S' do
    expect(upcased_and_line_spaced(sandwich.taste)).to eq('D E L I C I O U S')
  end
  it 'lets me add toppings' do
    sandwich.toppings << 'cheese'
    expect(sandwich.toppings).not_to be_empty
  end
end
Listing 5-16

Example of Custom Metadata

The enhancement is on line 3 only (Figure 5-4).
../images/497830_1_En_5_Chapter/497830_1_En_5_Fig4_HTML.jpg
Figure 5-4

Using Metadata

You have attached the metadata test: :unit.

Metadata are available inside the example code. They are available via the example reference that can optionally be defined as the argument to the example do..end block. See how the example is displayed as D E L I C I O U S has access to this variable and to the metadata (Listing 5-17).
# File: spec/sandwich_spec.rb
#
RSpec.describe Sandwich, test: :unit do
  subject(:sandwich) do
    described_class.new('delicious', [])
  end
  it 'is delicious' do
    expect(sandwich.taste).to eq('delicious')
  end
  it 'is displayed as D E L I C I O U S' do |example|
    puts "Type of test: #{example.metadata[:test]}"
    expect(upcased_and_line_spaced(sandwich.taste)).to eq('D E L I C I O U S')
  end
  it 'lets me add toppings' do
    sandwich.toppings << 'cheese'
    expect(sandwich.toppings).not_to be_empty
  end
end
Listing 5-17

Access Metadata

In Figure 5-5, I am highlighting the relevant parts of the code.
../images/497830_1_En_5_Chapter/497830_1_En_5_Fig5_HTML.jpg
Figure 5-5

Metadata Accessed in Example Code

Note that the metadata are being inherited from the example group to all the enclosed examples and other example groups (describes or contexts). And each contained example group or example can add more metadata or override the value of metadata inherited.

See how spec on line 18 now overrides the value of the :test metadata key (Figure 5-6).
../images/497830_1_En_5_Chapter/497830_1_En_5_Fig6_HTML.jpg
Figure 5-6

Override Metadata Values

The new version of the spec/sandwich_spec.rb is as shown in Listing 5-18.
# File: spec/sandwich_spec.rb
#
RSpec.describe Sandwich, test: :unit do
  subject(:sandwich) do
    described_class.new('delicious', [])
  end
  it 'is delicious' do
    expect(sandwich.taste).to eq('delicious')
  end
  it 'is displayed as D E L I C I O U S' do |example|
    puts "Type of test: #{example.metadata[:test]}"
    expect(upcased_and_line_spaced(sandwich.taste)).to eq('D E L I C I O U S')
  end
  it 'lets me add toppings', test: :integration do |example|
    puts "Type of test: #{example.metadata[:test]}"
    sandwich.toppings << 'cheese'
    expect(sandwich.toppings).not_to be_empty
  end
end
Listing 5-18

Override Metadata Values

Now, let’s run the spec/sandwich_spec.rb specs:
$ bundle exec rspec spec/sandwich_spec.rb
Randomized with seed 65045
This is executed only once at the beginning of the specifications run
Sandwich
Type of test: integration
  lets me add toppings
Type of test: unit
  is displayed as D E L I C I O U S
  is delicious
Finished in 0.00841 seconds (files took 0.21139 seconds to load)
3 examples, 0 failures
Randomized with seed 65045
$

As you can see in the preceding code, the lets me add toppings has type of test integration, whereas the is displayed as D E L I C I O U S has type of test unit.

Finally, if you want to set a metadata key to the value true, you can always give the key without the value. Here is an example:
it 'order contains 5 items`, :wip do
   ...
end

In this example, you are attaching the metadata key :wip. This is attached with the value true, even if you have not explicitly set that.

Filtering

Metadata are also useful with regard to what examples you might want to run when invoking rspec. In other words, you can limit the selected examples according to their metadata values.

Here is how you can invoke rspec using metadata:
$ bundle exec rspec --tag test:integration
Run options: include {:test=>"integration"}
Randomized with seed 43976
This is executed only once at the beginning of the specifications run
Sandwich
Type of test: integration
  lets me add toppings
Finished in 0.00777 seconds (files took 0.20195 seconds to load)
1 example, 0 failures
Randomized with seed 43976
$

As you can see in the preceding code, you have invoked rspec with the --tag test:integration. This filtered the examples to run only the ones that have the metadata tag test with value integration.

Tasks and Quizzes

This chapter is coming both with a quiz and a task.

Quiz

The quiz for this chapter can be found here: www.techcareerbooster.com/quizzes/useful-rspec-tools.

Task Details

Write a Ruby Class And Cover It With RSPEC SPECS
You will need to write a Ruby class that would satisfy the following requirements:
NumberEachLine
  prefixes each line with an increasing integer number starting at 1
  when it is configured to start the line count at 8
    prefixes each line with an increasing integer number starting at 8
  when it is configured to pad each number with leading zeros
    prefixes each line with an increasing integer padded with leading zeros
    when it is configured to pad each number with leading blanks
      prefixes each line with an increasing integer padded with leading blanks
    when number of lines does not justify padding
      does not pad with leading zeros
  when it is configured to prefix each line with #
    prefixes each line with an increasing integer number starting at 1 and prefix each line being #
    and when it is configured to pad each number with leading zeros
      prefixes each line with an increasing integer number starting at 1 and pads each number with leading zeros and prefixes the line with #
  when it is configured to suffix each line number with ". "
    prefixes each line with an increasing integer number starting at 1 and suffix on numbers being ". "
  when input is
class Hello
  def initialize(name)
    @name = name
  end
  def greetings
    puts "Hello RSpec::ExampleGroups::NumberEachLine"
  end
  def name
    @name
  end
end
 and we configure pad of numbers with blanks and suffix number with '. '
    generates the following
 1. class Hello
 2.   def initialize(name)
 3.     @name = name
 4.   end
 5.
 6.   def greetings
 7.     puts "Hello RSpec::ExampleGroups::NumberEachLine"
 8.   end
 9.
10.   def name
11.     @name
12.   end
13. end
Finished in 0.00232 seconds (files took 0.07316 seconds to load)
9 examples, 0 failures
The preceding code is a sample output of the RSpec dry run for the requirements of the class. And the following screenshot is a colored output that might be more helpful to you (Figure 5-7).
../images/497830_1_En_5_Chapter/497830_1_En_5_Fig7_HTML.png
Figure 5-7

Requirements of the Task

But let me give you some more details:
  1. (1)

    The purpose of this class is to take as input a multiline string and return back each line of the string being suffixed with its corresponding line number. For example, if you have the string

    This is a multi-line
    string. This is second line
    And this is third line
    the class should return back this:
    1This is a multi-line
    2string. This is second line
    3And this is third line
     
  2. (2)
    Besides this main functionality, the class should allow for some kind of configuration. For example, you want to tell that each number should be suffixed with the string ". ". If you say that, then the returned string should be something like this:
    1. This is a multi-line
    2. string. This is second line
    3. And this is third line
     
  3. (3)
    Or you may say that the line numbering should start from 8, not 1. In that case, the output should be something like this:
    8This is a multi-line
    9string. This is second line
    10And this is third line
     
  4. (4)
    You may also say that you want each number to be padded with 0s or other character sequence. For example, assuming that you want the numbers to be padded with blanks, the input is
    This is a multi-line
    string. This is a second line
    And this is the third line
    And this is the fourth line
    And this is the fifth line
    And this is the sixth line
    And this is the seventh line
    And this is the eighth line
    And this is the ninth line
    And this is the tenth line
    And this is the eleventh line
    And then the output will be
     1This is a multi-line
     2string. This is a second line
     3And this is the third line
     4And this is the fourth line
     5And this is the fifth line
     6And this is the sixth line
     7And this is the seventh line
     8And this is the eighth line
     9And this is the ninth line
    10And this is the tenth line
    11And this is the eleventh line
     
  5. (5)
    However, when the number of lines does not justify padding, then padding will not be used even if specified. Hence, if you specify that you want padding with 0s but the input is less than ten lines long, then no padding will take place. In other words, the input
    line 1
    line 2
    is output as
    1line 1
    2line 2
     
  6. (6)

    You may also ask the class to prefix each line with a prefix string. Assume that you have the following input and the prefix you want to attach to each line is '#':

    line 1
    line 2
     
Then the output will be
#1line1
#2line2
So let’s summarize the configuration options:
  1. 1.

    Starting Number: Default value is 1.

     
  2. 2.

    Pad Numbers With: A string to pad numbers with. Numbers are right justified, and the padding string is repeated to the left.

     
  3. 3.

    Suffix Number With: A string to suffix each number with.

     
  4. 4.

    Prefix Line With: A string to prefix each line with.

     

Please note that you don’t have to stick to the RSpec specifications that would give the same output as the one presented at the top of this task. You can come up with your own implementation for the RSpec specifications. What I want you to do is to implement the class and make sure that it is well test covered.

Note that this task is inspired by this online text mechanics page: http://textmechanic.com/text-tools/numeration-tools/number-each-line/.

Key Takeaways

  • How to use RSpec hooks

  • How to use subject and named subjects

  • How to extend RSpec with helper methods

  • How to use RSpec metadata

In the following chapter, you will be introduced to another very popular tool that lives in the BDD (Behavior-Driven Development): Cucumber. Cucumber allows you to write your specifications in plain English.

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

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