Slide 1

Slide 1 text

Bradley Schaefer (@soulcutter) Table XI (www.tablexi.com)

Slide 2

Slide 2 text

Who am I?

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

rspec-core

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

aggregate_failures F.F Failures: 1) User when registered should be registered Failure/Error: it { is_expected.to be_registered } expected `#.registered?` to return true, got false # ./spec/user_old_spec.rb:11:in `block (3 levels) in ' 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 `#.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 ' Finished in 0.01499 seconds (files took 0.11958 seconds to load) 3 examples, 2 failures

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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 `#.registered?` to return true, got false # ./spec/user_spec.rb:12:in `block (3 levels) in ' 1.2) Failure/Error: expect(user).to be_registered(registration_time) expected `#.registered?(1969-12-31 19:00:01.000000000 -0500)` to return true, got false # ./spec/user_spec.rb:13:in `block (3 levels) in ' Finished in 0.02684 seconds (files took 0.15189 seconds to load) 1 example, 1 failure

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

rspec-mocks Ha ha!

Slide 14

Slide 14 text

spies spy("IO") == double("IO").as_null_object

Slide 15

Slide 15 text

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)

Slide 16

Slide 16 text

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 ' 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 ' fred = instance_double("User", registered?: true) finder = class_double("User", find: fred)

Slide 17

Slide 17 text

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!

Slide 18

Slide 18 text

verifying doubles fred = instance_spy("User", registered?: true) finder = class_spy("User", find: fred) verifying SPIES

Slide 19

Slide 19 text

rspec-expectations

Slide 20

Slide 20 text

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 ' 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 '

Slide 21

Slide 21 text

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 '

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

compound matchers http://tiny.cc/rspec-json { "name" : { "first" : "Joe", "last" : "Sixpack" }, "gender" : true, "registered_at" : "2015-11-05" }

Slide 24

Slide 24 text

rspec-rails

Slide 25

Slide 25 text

rails_helper.rb

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

spec_helper.rb* RSpec.configure do |config| config.expect_with :rspec do |expectations| expectations.include_chain_clauses_in_custom_matcher_descriptions = true end config.disable_monkey_patching! config.order = :random Kernel.srand config.seed end

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

No content

Slide 30

Slide 30 text

Bradley Schaefer (@soulcutter) Table XI (www.tablexi.com)

Slide 31

Slide 31 text

No content

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

Bradley Schaefer (@soulcutter) Table XI (www.tablexi.com)