The RSpec Toolkit
David Chelimsky
7 Sept 2012
Windy City Rails
David Chelimsky
7 Sept 2012
Windy City Rails
group = describe Something do end pp group.metadata # {:example_group=> # {:description_args=>[Something], # :caller=> [ ... ]}}
example = nil describe Something do example = it "does something" do end end pp example.metadata # { :example_group => { ... }, # :description_args => ["does something"], # :caller=>[ ... ] # }
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
# 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
--format progress --format documentation --format html --format textmate --format json # coming in rspec-2.12
def example_passed(example) puts example.metadata[:description] end
def dump_commands_to_rerun_failed_examples puts "rspec #{example.metadata[:location]}") end
example.metadata[:execution_result][:run_time]
--require rspec/instafail --format RSpec::Instafail --format Fuubar --format NyanCatMusicFormatter
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 # }
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 # }}
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 # }
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 # }
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 # }
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
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
# 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
RSpec.configure do |config| config.include FakeFS::SpecHelpers, :fakefs end describe "something that writes files", :fakefs => true do # ... end
describe Account do it "accepts deposits" do end it "prevents overdraft", :pending => true do end end # { :description_args => ["prevents overdraft"], # :pending => true # }
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
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
# 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.
# 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
--color --order random --format progress --profile
--color
# load current-iteration.opts instead of ./.rspec and ~/.rspec rspec --options current-iteration.opts
# in ./current-iteration.opts --tag iteration:27
or, with --options option
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
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"
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_/ }
# since rspec-expectations-2.11.0 RSpec.configure do |c| c.add_should_and_should_not_to ActiveRecord::CollectionProxy end
# 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"
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
# 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}
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
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
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
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
# 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"
$ 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
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