Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

The Humble Example it "will do the correct thing" do expect(important_thing).to be_valid end

Slide 3

Slide 3 text

The Context RSpec.describe do context "in most cases" do it "will do the correct thing" do end end end

Slide 4

Slide 4 text

Hooks, metadata and filters • before(:example) / after(:example) / around(:example) • before(:context) / after(:context) • before(:suite) / after(:suite)

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

Hooks, metadata and filters • Metadata can be used to run certain subsets of specs via filtering • rspec --tags type:awesome

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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?

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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)

Slide 20

Slide 20 text

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 }

Slide 21

Slide 21 text

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?

Slide 22

Slide 22 text

Custom Matchers RSpec::Matchers.define :matcher_name do |expected| match do |actual| # ... logic end end

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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?

Slide 25

Slide 25 text

What do we do when we have failures? • Lots of customisation on running the suite, what about after?

Slide 26

Slide 26 text

Bisect • Allows you to find the minimum failure set for a seed. • rspec --seed 1234 --bisect • Has both a shell and a fork runner.

Slide 27

Slide 27 text

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:

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

Thanks @JonRowe