The RSpec Toolkit

David Chelimsky
7 Sept 2012
Windy City Rails











Each example has its own metadata, and you can add filters or modules to run/disable/enhance examples based on that metadata at run time.

rspec-core-2.0 < micronaut




example group metadata

group = describe Something do
end

pp group.metadata

# {:example_group=>
#   {:description_args=>[Something],
#    :caller=> [ ... ]}}

example metadata

example = nil

describe Something do
  example = it "does something" do
  end
end

pp example.metadata

# { :example_group => { ... },
#   :description_args => ["does something"],
#   :caller=>[ ... ]
# }

metadata - lazy keys

describe Something do
  it "does something" do
    # ...
  end
end

example.metadata[:file_path]        # path/to/something_spec.rb
example.metadata[:line_number]      # 2
example.metadata[:location]         # path/to/something_spec.rb:2
example.metadata[:description]      # does something
example.metadata[:full_description] # Something does something

metadata in formatters

# in any formatter:

def start(example_count); end

def example_group_started(example_group); end

def example_group_finished(example_group); end

def example_started(example); end

def example_passed(example); end

def example_pending(example); end

def example_failed(example); end

built-in formatters

--format progress
--format documentation
--format html
--format textmate
--format json # coming in rspec-2.12

documentation formatter

def example_passed(example)
  puts example.metadata[:description]
end

replay commands (console formatters)

def dump_commands_to_rerun_failed_examples
  puts "rspec #{example.metadata[:location]}")
end

html formatter

example.metadata[:execution_result][:run_time]

3rd-party formatters

--require rspec/instafail --format RSpec::Instafail
--format Fuubar
--format NyanCatMusicFormatter

filters

focus on example

RSpec.configure do |config|
  config.run_all_when_everything_filtered = true
  config.filter_run :focus => true
end

describe Account do
  it "accepts deposits", :focus => true do
  end

  it "prevents overdraft" do
  end
end

# { :example_group => { ... },
#   :description_args => ["accepts deposits"],
#   :focus => true
# }

focus on group

RSpec.configure do |config|
  config.run_all_when_everything_filtered = true
  config.filter_run :focus => true
end

describe Account, :focus => true do
  it "accepts deposits" do
  end

  it "prevents overdraft" do
  end
end

# { :example_group => {
#     :description_args => [Account],
#     :focus => true
# }}

exclusions

RSpec.configure do |config|
  config.run_all_when_everything_filtered = true
  config.filter_run_excluding :slow => true
end

describe Account do
  it "accepts deposits", :slow => true do
  end

  it "prevents overdraft" do
  end
end

# { :example_group => { ... },
#   :description_args => ["accepts deposits"],
#   :slow => true
# }

arbitrary metadata

RSpec.configure do |config|
  config.filter_run_including :iteration => 1
end

describe Account do
  it "accepts deposits", :iteration => 1 do
  end

  it "prevents overdraft" do
  end
end

# { :example_group => { ... },
#   :description_args => ["accepts deposits"],
#   :iteration => 1
# }

symbols

RSpec.configure do |config|
  # this will be the default in rspec-3.0
  config.treat_symbols_as_metadata_keys_with_true_values = true
end

describe Account do
  it "accepts deposits", :foo do
  end

  it "prevents overdraft" do
  end
end

# { :example_group => { ... },
#   :description_args => ["accepts deposits"],
#   :foo => true
# }

conditional filters

describe Ruby19OnlyFeature, :if => RUBY_VERSION >= "1.9" do
  # ...
end

describe NotOn186, :unless => RUBY_VERSION == "1.8.6" do
  # ...
end

describe "generate gold master", :if => ENV['GENERATE'] do
  # ...
end

before/after hooks

RSpec.configure do |config|
  config.before(:each, :load_settings) do
    MyApp::Settings.load # expensive, but memoized
  end
end

describe "something that needs settings", :load_settings do
  # ...
end

describe "something that doesn't need settings" do
  # ...
end

specialized groups

# in rspec-rails

RSpec::configure do |c|
  def c.escaped_path(*parts)
    Regexp.compile(parts.join('[\\\/]'))
  end

  c.include RSpec::Rails::RequestExampleGroup,
    :type => :request,
    :example_group => {
      :file_path => c.escaped_path(%w[spec (requests|integration)])
    }

  c.include RSpec::Rails::ControllerExampleGroup,
    :type => :controller,
    :example_group => {
      :file_path => c.escaped_path(%w[spec controllers])
    }
end

include helper module

RSpec.configure do |config|
  config.include FakeFS::SpecHelpers, :fakefs
end

describe "something that writes files", :fakefs => true do
  # ...
end

pending

describe Account do
  it "accepts deposits" do
  end

  it "prevents overdraft", :pending => true do
  end
end

# { :description_args => ["prevents overdraft"],
#   :pending => true
# }

aliases

RSpec.configuration do |c|
  # these two are built-in
  c.alias_example_to :focus,   :focus   => true
  c.alias_example_to :pending, :pending => true
end

describe Account do
  # it "accepts deposits", :focus => true do
  focus "accepts deposits" do
  end

  # it "prevents overdraft", :pending => true do
  pending "prevents overdraft" do
  end
end
RSpec.configuration do |c|
  # ... and so is this
  c.alias_example_to :xit,
                     :pending => 'Temporarily disabled with xit'
end

describe Account do
  # it "prevents overdraft",
  #    :pending => 'Temporarily disabled with xit'
  xit "prevents overdraft" do
  end

  it "accepts deposits" do
  end
end
RSpec.configuration do |c|
  # Add your own:
  c.alias_example_to :next,
                     :pending => 'Next iteration'
end

describe Account do
  it "accepts deposits" do
  end

  next "prevents overdraft" do
  end
end

configuration

RSpec.configuration

RSpec.configuration do |c|
  # attributes
  c.default_path = "./behavior"
  c.output_stream = File.new("./rspec.out")
  c.error_stream = File.new("./rspec.err")
  c.fail_fast = true
  # will be default in rspec-3.0
  c.treat_symbols_as_metadata_keys_with_true_values = true

  # methods
  c.filter_run           :focus # including
  c.filter_run_including :focus
  c.filter_run_excluding :slow
  c.mock_with :mocha
  c.expect_with :stdlib
end
      

command line

# display all command line options

rspec --help
Usage: rspec [options] [files or directories]

    -I PATH                          Specify PATH to add to $LOAD_PATH (may be used more than once).
    -r, --require PATH               Require a file.
    -O, --options PATH               Specify the path to a custom options file.
        --order TYPE[:SEED]          Run examples by the specified order type.
                                       [default] files are ordered based on the underlying file
                                                 system's order
                                       [rand]    randomize the order of files, groups and examples
                                       [random]  alias for rand
                                       [random:SEED] e.g. --order random:123
        --seed SEED                  Equivalent of --order rand:SEED.


      

command line - tags

# tag is synonymous w/ filter

rspec --tag focus:true # adds to any hard-coded filters
rspec --tag ~slow:true # examples _not_ tagged w/ slow:true
rspec --tag ~slow:true --tag issue:137 # tags are additive

rspec --tag focus
rspec --tag ~slow
rspec --tag ~slow --tag issue:137
      

local options: ./.rspec

--color
--order random
--format progress
--profile

global options: ~/.rspec

--color

"profiles" using --options

# load current-iteration.opts instead of ./.rspec and ~/.rspec
rspec --options current-iteration.opts
# in ./current-iteration.opts
--tag iteration:27

precedence

or, with --options option

pro tip

Keep structural configuration (before/after hooks, includes, etc) in RSpec.configuration and keep run-time configuration (filters, order, fail-fast, etc) on the command line and in options files.

# in spec/spec_helper.rb
RSpec.configuration do |config|
  config.include FakeFS::SpecHelpers, :fakefs

  # exception to this guideline
  config.run_all_when_everything_filtered = true
  config.filter_run_including :focus
end

# in .rspec
--color
--format documentation

rspec-expectations

basic syntax: should

actual.should matcher
actual.should matcher, message
actual.should_not matcher
actual.should_not matcher, message

#examples
account.balance.should eq Money.new(1_000_000, :USD)
account.overdrawn?.should be_false,
  "expected account not to be overdrawn"
list.should_not be_empty
list.should_not be_empty, "expected at least one item"

problem

article.comments.should # NoMethodError

# because ActiveRecord::CollectionProxy removes most
# methods (including should and should_not)
instance_methods.each { |m|
  undef_method m unless
  m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to|proxy_/
 }

workaround

# since rspec-expectations-2.11.0

RSpec.configure do |c|
  c.add_should_and_should_not_to ActiveRecord::CollectionProxy
end

solution: expect [not_]to

# optional syntax since rspec-expectations-2.11.0
expect(actual).to matcher
expect(actual).to matcher, message
expect(actual).not_to matcher
expect(actual).not_to matcher, message

#examples
expect(account.balance).to eq Money.new(1_000_000, :USD)
expect(account.overdrawn?).to be_false,
  "expected account not to be overdrawn"
expect(list).not_to be_empty
expect(list).not_to be_empty, "expected at least one item"

operator matchers

actual.should == expected      |   # not supported with expect
actual.should =~ expected      |   # not supported with expect
actual.should === expected     |   # not supported with expect
                               |
actual.should be < expected    |   expect(actual).to be < expected
actual.should be <= expected   |   expect(actual).to be <= expected
actual.should be >= expected   |   expect(actual).to be >= expected
actual.should be > expected    |   expect(actual).to be > expected

block matchers

# underlying
lambda { ... }.should matcher
lambda { ... }.should_not matcher

# original "expect" DSL
expect { ... }.to matcher
expect { ... }.not_to matcher

# examples
expect { something }.to raise_exception
expect { something }.not_to change{Widget.count}

subject

describe Article do
  it "validates presence of title" do
    article = Article.new(:title => nil)
    article.valid?.should be_false
    article.errors[:title].should include "can't be blank"
  end
end
describe Article do
  it "validates presence of title" do
    article = Article.new(:title => nil)
    article.valid?.should be_false
    article.errors[:title].should include "can't be blank"
  end

  it "validates presence of author" do
    article = Article.new(:author => nil)
    article.valid?.should be_false
    article.errors[:author].should include "can't be blank"
  end
end
describe Article do
  it "validates presence of title" do
    described_class.new.should validate_presence_of :title
  end

  it "validates presence of author" do
    described_class.new.should validate_presence_of :author
  end
end
describe Article do
  it { should validate_presence_of :title }
  it { should validate_presence_of :author }
end
# output
Article
  should validate presence of :title
  should validate presence of :author
describe Article do
  it { should validate_presence_of :title }  # nail!
  it { should validate_presence_of :author } # nail!

  it { should have_many :comments }          # screw!
end

validations are behavior
associations are structure

describe Article do
  it { should validate_presence_of :title }
  it { should validate_presence_of :author }

  it "sorts comments in reverse by default" do
    article = Article.create!
    comments = [
      article.comments.create!,
      article.comments.create!
    ]
    article.comments.should eq comments.reverse
  end
describe Account do
  # good: generic statement about all accounts
  it { should validate_presence_of :owner }

  # bad: specific statement about a specific account
  its(:balance) { should eq Money.new(0, :USD) }
end

# Account
#   should validate presence of :owner
#   balance
#     should eq $0
describe Account do
  it { should validate_presence_of :owner }

  # slightly less bad given some context
  describe "defaults" do
    its(:balance) { should eq Money.new(0, :USD) }
  end
end

# Account
#   should validate presence of :owner
#   defaults
#     balance
#       should eq $0
describe Account do
  it { should validate_presence_of :owner }

  # better: provides necessary context with no less code
  # but in a simpler, more readable form
  it "has a default balance of $0" do
    Account.new.balance.should eq Money.new(0, :USD)
  end
end

# Account
#   should validate presence of :owner
#   has a default balance of $0
describe Account do
  it { should validate_presence_of :owner }

  describe "defaults" do
    its(:balance) { should eq Money.new(0, :USD) }
  end

  context "after a deposit" do
    subject { Account.new.tap { |a| a.deposit Money.new(25, :USD) } }
    its(:balance) { should eq Money.new(25, :USD) }
  end
end

# Account
#   should validate presence of :owner
#   defaults
#     balance
#       should eq $0
#   after a deposit
#     balance
#       should eq $25
describe Account do
  it { should validate_presence_of :owner }

  it "has a default balance of $0" do
    Account.new.balance.should eq Money.new(0, :USD)
  end

  it "increases its balance by amount deposited" do
    account = Account.new
    account.deposit Money.new(25, :USD)
    expect(account.balance).to eq Money.new(25, :USD)
  end
end

# Account
#   should validate presence of :owner
#   has a default balance of $0
#   increases its balance by amount deposited

readability

start simple

describe Person do
  describe "#full_name" do
    it "concats first_name and last_name" do
      person = Person.new :first_name => "Ray",
                          :last_name => "Hightower"
      expect(person.full_name).to eq "Ray Hightower"
    end
  end
end

assert
(if that's how you roll)

describe Person do
  describe "#full_name" do
    it "concats first_name and last_name" do
      person = Person.new :first_name => "Ray",
                          :last_name => "Hightower"
      assert_equal "Ray Hightower", person.full_name
    end
  end
end

pro tip

# when you see this:
assert_equal "Ray", person.first_name

# think this:
assert_equal_Ray person.first_name

# and you're less likely to do this:
assert_equal person.first_name, "Ray"
# => Expected: "Joe"
# =>   Actual: "Ray"

wrong

$ gem install wrong
require 'wrong/adapters/rspec'

describe Person do
  describe "#full_name" do
    it "concats first_name and last_name" do
      person = Person.new :first_name => "Steve",
                          :last_name => "Conover"
      expect_that { person.full_name == "Steve Conover" }
    end
  end
end

tell a story

describe Person do
  describe "#full_name" do
    it "concats first_name and last_name"
    it "ignores a nil first_name"
    it "ignores a nil last_name"
  end
end

# Person
#   #full_name
#     concats first_name and last_name
#     ignores a nil first_name
#     ignores a nil last_name

?? questions ??