Upgrade to Pro — share decks privately, control downloads, hide ads and more …

RSpec SECRETS

RSpec SECRETS

If you've written Ruby there's a good chance that at some point you've used RSpec to write automated tests for your code. When RSpec 3.0 was released in 2014 it gained a lot of really powerful features that often go overlooked. I'm going to share with you a bag of tricks that will take you from writing "basically adequate" tests to using the full power of RSpec to write magnificent specs. These features will help you to write clearer and more expressive tests.

Bradley Schaefer

November 05, 2015
Tweet

More Decks by Bradley Schaefer

Other Decks in Programming

Transcript

  1. RSpec 3.x • 3.0.0 released June 2014 • 4,069 commits,

    122 contributors since 2.14 • 3.3.0 released June 2015 • 2,331 commits, 135 contributors since 3.0.0
  2. rspec-expectations rspec-mocks rspec-rails rspec-core • aggregate_failures • only-failures • bisect

    • composable matchers • compound matchers • spies • verifying doubles • rails_helper.rb • testing jobs
  3. aggregate_failures class User attr_accessor :registered_at def registered?(time = Time.now) false

    end end require 'spec_helper' require 'user' RSpec.describe User do context "when registered" do subject(:user) do User.new.tap { |u| u.registered_at = registration_time } end let(:registration_time) { Time.at(1) } it { is_expected.to be_registered } it { is_expected.to be_registered(registration_time) } it { is_expected.not_to be_registered(registration_time - 1) } end end
  4. aggregate_failures F.F Failures: 1) User when registered should be registered

    Failure/Error: it { is_expected.to be_registered } expected `#<User:0x007fede10cfe50 @registered_at=1969-12-31 19:00:01 -0500>.registered?` to return true, got false # ./spec/user_old_spec.rb:11:in `block (3 levels) in <top (required)>' 2) User when registered should be registered 1969-12-31 19:00:01.000000000 -0500 Failure/Error: it { is_expected.to be_registered(registration_time) } expected `#<User:0x007fede118fb38 @registered_at=1969-12-31 19:00:01 -0500>.registered?(1969-12-31 19:00:01.000000000 -0500)` to return true, got false # ./spec/user_old_spec.rb:12:in `block (3 levels) in <top (required)>' Finished in 0.01499 seconds (files took 0.11958 seconds to load) 3 examples, 2 failures
  5. require 'spec_helper' require 'user' RSpec.describe User do context "when registered"

    do subject(:user) do User.new.tap { |u| u.registered_at = registration_time } end let(:registration_time) { Time.at(1) } it "keeps track of the registration time", :aggregate_failures do expect(user).to be_registered expect(user).to be_registered(registration_time) expect(user).not_to be_registered(registration_time - 1) end end end aggregate_failures class User attr_accessor :registered_at def registered?(time = Time.now) false end end
  6. aggregate_failures F Failures: 1) User when registered keeps track of

    the registration time Got 2 failures: 1.1) Failure/Error: expect(user).to be_registered expected `#<User:0x007fae20a0c0a0 @registered_at=1969-12-31 19:00:01 -0500>.registered?` to return true, got false # ./spec/user_spec.rb:12:in `block (3 levels) in <top (required)>' 1.2) Failure/Error: expect(user).to be_registered(registration_time) expected `#<User:0x007fae20a0c0a0 @registered_at=1969-12-31 19:00:01 -0500>.registered?(1969-12-31 19:00:01.000000000 -0500)` to return true, got false # ./spec/user_spec.rb:13:in `block (3 levels) in <top (required)>' Finished in 0.02684 seconds (files took 0.15189 seconds to load) 1 example, 1 failure
  7. only-failures $ rspec FF.F Finished in 0.02459 seconds (files took

    0.1419 seconds to load) 4 examples, 3 failures $ rspec --only-failures FFF Finished in 0.02406 seconds (files took 0.1662 seconds to load) 3 examples, 3 failures $ rspec --next-failure spec/user_spec.rb F Finished in 0.01846 seconds (files took 0.1286 seconds to load) 1 example, 1 failure RSpec.configure do |config| config.example_status_persistence_file_path = "rspec.txt" config.run_all_when_everything_filtered = true end
  8. bisect Bisect started using options: "--seed 1234" Running suite to

    find failures... (0.16755 seconds) Starting bisect with 1 failing example and 9 non-failing examples. Round 1: searching for 5 non-failing examples (of 9) to ignore: .. (0.30166 seconds) Round 2: searching for 3 non-failing examples (of 5) to ignore: .. (0.30306 seconds) Round 3: searching for 2 non-failing examples (of 3) to ignore: .. (0.33292 seconds) Round 4: searching for 1 non-failing example (of 2) to ignore: . (0.16476 seconds) Round 5: searching for 1 non-failing example (of 1) to ignore: . (0.15329 seconds) Bisect complete! Reduced necessary non-failing examples from 9 to 1 in 1.26 seconds. The minimal reproduction command is: rspec ./spec/calculator_10_spec.rb[1:1] ./spec/calculator_1_spec.rb[1:1] --seed 1234 $ rspec --seed 1234 --bisect
  9. spies io = double("IO") expect(io).to receive(:puts) io.puts "May the force

    be with you" Arrange Act Assert io = spy("IO") io.puts "May the force be with you" expect(io).to have_received(:puts)
  10. verifying doubles Failures: 1) User doubles verify arguments Failure/Error: expect(fred).to

    be_registered(1, 2, 3) ArgumentError: Wrong number of arguments. Expected 0 to 1, got 3. # ./double_spec.rb:15:in `block (2 levels) in <top (required)>' 2) User doubles verify methods are defined Failure/Error: class_double("User", find_by: 1) the User class does not implement the class method: find_by # ./double_spec.rb:19:in `block (2 levels) in <top (required)>' fred = instance_double("User", registered?: true) finder = class_double("User", find: fred)
  11. verifying doubles RSpec.configure do |config| config.mock_with :rspec do |mocks| mocks.verify_partial_doubles

    = true end end user = User.new expect(user).to receive(:saave) # nope!
  12. composable matchers RSpec.describe "Without composable matchers" do let(:hash) {{foo: :bar}}

    it "has bad error messages" do expect(hash[:something][:nested]).to eq "Thundercats" end it "requires extra assertions" do expect(hash[:something]).to be expect(hash[:something][:nested]).to eq "Thundercats" end end Failures: 1) Without composable matchers has bad error messages Failure/Error: expect(hash[:something][:nested]).to eq "Thundercats" NoMethodError: undefined method `[]' for nil:NilClass # ./composable_spec.rb:5:in `block (2 levels) in <top (required)>' 2) Without composable matchers requires extra assertions Failure/Error: expect(hash[:something]).to be expected nil to evaluate to true # ./composable_spec.rb:9:in `block (2 levels) in <top (required)>'
  13. composable matchers RSpec.describe "With composable matchers" do let(:hash) {{foo: :bar}}

    it "is expressive and has good errors" do expect(hash).to include( foo: a_hash_including(nested: "Thundercats") ) end end Failures: 1) With composable matchers is expressive and has good errors Failure/Error: expect(hash).to include( expected {:foo => :bar} to include {:foo => (a hash including {:nested => "Thundercats"})} Diff: @@ -1,2 +1,2 @@ -[{:foo=>(a hash including {:nested => "Thundercats"})}] +:foo => :bar, # ./composable_spec.rb:5:in `block (2 levels) in <top (required)>'
  14. compound matchers RSpec.describe "Compound matchers" do it "combines expectations with

    logical OR" do expect([]).to be_a(Hash).or be_an(Array) expect(nil).to be_a(Hash) | be_an(Array) end it "combines expectations with logical AND" do expect("one two three") .to start_with("one").and end_with("three") expect("one two three four") .to start_with("one") & end_with("three") end end
  15. compound matchers http://tiny.cc/rspec-json { "name" : { "first" : "Joe",

    "last" : "Sixpack" }, "gender" : true, "registered_at" : "2015-11-05" }
  16. rails_helper.rb ActiveRecord::Migration.maintain_test_schema! RSpec.configure do |config| config.filter_rails_from_backtrace! # RSpec Rails can

    automatically mix in different behaviours to your tests # based on their file location # # You can instead explicitly tag your specs with their type, e.g.: # # RSpec.describe UsersController, :type => :controller do # # The different available types are documented in the features, such as in # https://relishapp.com/rspec/rspec-rails/docs config.infer_spec_type_from_file_location! end
  17. RSpec.describe "activejob" do let(:admirably) { Class.new(ActiveJob::Base) { def perform(*); end

    } } it "performs admirably" do later = Time.now + 300 expect { admirably.set(wait_until: later) .perform_later("swagger") }.to have_enqueued_job.with("swagger").at(later) end end testing jobs* rails/activejob
  18. yield matchers RSpec.describe "yield matchers" do it "verifies several yielding

    scenarios", :aggregate_failures do expect { |block| Foo.bar(&block) }.to yield_control expect { |block| Foo.bar(&block) }.to yield_control.twice expect { |block| Foo.bar(&block) }.to yield_with_args(/\A\d+\z/) expect { |block| Foo.bar(&block) }.to yield_with_no_args expect { |block| Foo.bar(&block) }.to yield_successive_args(Fixnum, Fixnum) end end
  19. stub_const class Foo; end RSpec.describe "stub_const" do it "stubs pre-existing

    constants" do fake_foo = Struct.new(:stuff) stub_const("Foo", fake_foo) expect(Foo).to eq fake_foo end it "stubs undefined constants" do stub_const("Foo::Job::RETRIES", 1) expect(Foo::Job::RETRIES).to eq 1 end end