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.
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
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
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
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
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
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
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?
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)) )
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
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)
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?
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
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?
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:
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
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