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

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

More Decks by Jon Rowe

Other Decks in Programming


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

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

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

    • before(:context) / after(:context) • before(:suite) / after(:suite)
  4. 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
  5. Hooks, metadata and filters • Metadata can be used to

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

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

    upon: RSpec.configure do |c| c.filter_run_when_matching :focus end
  8. 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
  9. 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
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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)>'
  15. 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?
  16. 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)) )
  17. 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
  18. 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)
  19. 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 }
  20. 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?
  21. 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
  22. 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?
  23. What do we do when we have failures? • Lots

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

    for a seed. • rspec --seed 1234 --bisect • Has both a shell and a fork runner.
  25. 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:
  26. 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
  27. 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