RSpec - Level Up

RSpec - Level Up

You've been using RSpec for a while, you know how to write tests, but you’re not sure you're getting the most from it. This is a quick run through of some of the lesser known, more advanced features of RSpec.


Jon Rowe

March 11, 2019


  1. RSpec - Level Up @JonRowe (One of the) RSpec Maintainer(s)

  2. The Humble Example it "will do the correct thing" do

    expect(important_thing).to be_valid end
  3. The Context RSpec.describe do context "in most cases" do it

    "will do the correct thing" do end end end
  4. Hooks, metadata and filters • before(:example) / after(:example) / around(:example)

    • before(:context) / after(:context) • before(:suite) / after(:suite)
  5. Hooks, metadata and filters • Metadata is a hash of

    information about an example RSpec.describe "Athing", this_is_metadata: "Hi!" do context this_is_metadata: "I'll override the top one" do end it also_has_meta_data: "I'll have both keys" do end end
  6. Hooks, metadata and filters • Metadata can be used to

    run certain subsets of specs via filtering • rspec --tags type:awesome
  7. Hooks, metadata and filters • Metadata can be created by

    aliases. RSpec.configure do |c| c.define_example_group_method :fdescribe, focus: true end
  8. Hooks, metadata and filters • Metadata can be conditionally filtered

    upon: RSpec.configure do |c| c.filter_run_when_matching :focus end
  9. Hooks, metadata and filters • Metadata can be used to

    conditionally include code, or run hooks RSpec.configure do |config| config.include SomeModule, type: :awesome config.before :example, type: :awesome do # all the things end end
  10. Hooks, metadata and filters • Can be used to apply

    ordering RSpec.configure do |config| config.register_ordering :global do |examples| acceptance, unit = examples.partition { |ex| ex.metadata[:acceptance] } unit.shuffle + acceptance.shuffle end end
  11. Hooks, metadata and filters • Can be used to apply

    ordering to specific things RSpec.configure do |config| config.register_ordering :ordered_subset do |examples| examples end end
  12. Hooks, metadata and filters • Used by rspec-rails to apply

    the different methods to examples • Built in configuration such as hooks: • skip: "Reason" • pending: "Reason" • :aggregate_failures
  13. Aggregate Failures it "wraps failures together", :aggregate_failures do expect(response.status).to eq(200)

    expect(response.headers).to include("Content-Type" => "application/json") expect(response.body).to eq('{"message":"Success"}') end
  14. Aggregate Failures it "wraps failures together" do aggregate_failures "testing response"

    do expect(response.status).to eq(200) expect(response.headers).to include("Content-Type" => "application/json") expect(response.body).to eq('{"message":"Success"}') end end
  15. Aggregate Failures 1) wraps failures together Got 3 failures: 1.1)

    Failure/Error: expect(response.status).to eq(200) expected: 200 got: nil (compared using ==) # ./spec/aggregate_failures_spec.rb:12:in `block (2 levels) in <top (required)>' 1.2) Failure/Error: expect(response.headers).to include("Content-Type" => "application/json") expected nil to include {"Content-Type" => "application/json"}, but it does not respond to `include?` # ./spec/aggregate_failures_spec.rb:13:in `block (2 levels) in <top (required)>' 1.3) Failure/Error: expect(response.body).to eq('{"message":"Success"}') expected: "{\"message\":\"Success\"}" got: nil (compared using ==) # ./spec/aggregate_failures_spec.rb:14:in `block (2 levels) in <top (required)>'
  16. One expectation per test? • How do we express complex

    expectations as one expectation • Compound matchers combine multiple assertions into one expectation • Consider an acceptance test that is checking a JSON response • Do we hard code a massive expected JSON response?
  17. Compound Matchers • With compound matchers we can express "complex"

    requirements easily expect(my_json).to match a_hash_including( 'something' => array_including(hash_including(id: 1)) )
  18. Compound Matchers • Matchers are reusable objects • Thus they

    present an easy way to create custom matchers def have_detail fragment a_hash_including( 'something' => array_including(hash_including(fragment)) ) end
  19. Compound Matchers • You can combine argument matchers in this

    fashion • You can combine normal matchers using and and/or or • Any matcher supporting values_match? and === expect( my_json ).to have_detail(id: 1).and have_detail(id: 2)
  20. Compound Matchers • Exception to the rule, negative matchers. •

    Instead create a "negated" matcher alias RSpec::Matchers.define_negated_matcher :preserve, :change expect { object.action }.to have_detail(id: 1).and preserve { object.value }
  21. Custom Matchers • You can alias matchers using RSpec::Matchers.alias_matcher •

    You can negate them using RSpec::Matchers.define_negated_matcher • But what if you want to create your own from scratch?
  22. Custom Matchers RSpec::Matchers.define :matcher_name do |expected| match do |actual| #

    ... logic end end
  23. Custom Matchers RSpec::Matchers.define :matcher_name do |expected| supports_block_expectations # actual will

    be a block diffable # failed matchers will attempt to diff expected / actual description { "..." } failure_message { "..." } failure_message_when_negated { "..." } chain :another_thing, { "..." } # new, allows you to chain multiple matchs end
  24. Custom Matchers • Are just Ruby • Require matches? and

    failure_message to be defined. • Optionally diffable? (requiring expected and actual) • Optionally supports_block_expectations? expects_call_stack_jump?
  25. What do we do when we have failures? • Lots

    of customisation on running the suite, what about after?
  26. Bisect • Allows you to find the minimum failure set

    for a seed. • rspec --seed 1234 --bisect • Has both a shell and a fork runner.
  27. Bisect Bisect started using options: "--seed 1234" Running suite to

    find failures... (0.17102 seconds) Starting bisect with 1 failing example and 9 non-failing examples. Checking that failure(s) are order-dependent... failure appears to be order-dependent Round 1: bisecting over non-failing examples 1-9 .. ignoring examples 6-9 (0.32943 seconds) Round 2: bisecting over non-failing examples 1-5 .. ignoring examples 4-5 (0.3154 seconds) Round 3: bisecting over non-failing examples 1-3 .. ignoring example 3 (0.2175 seconds) Bisect aborted! The most minimal reproduction command discovered so far is:
  28. Formatters • Formatters run all of RSpecs reporting. • Can

    build entirely custom implementations • Hook into the lifecycle of the test suite. • Subscribe to only the events you need
  29. Formatters class MyTotallyAmazingFormatter RSpec::Core::Formatters.register self, :example_failed, :example_passed def initialize output

    @output = output end def example_failed notification @output.puts "Bad times baz, we broke the build." end def example_passed notification @output.puts "Good news everyone!" end end
  30. Thanks @JonRowe