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

RSpec: It Isn't Actually Magic

RSpec: It Isn't Actually Magic

Noel Rappin

April 22, 2015
Tweet

More Decks by Noel Rappin

Other Decks in Technology

Transcript

  1. Once upon a time, there was a test… require 'rails_helper'


    
 RSpec.describe Name do
 it "produces a sort name" do
 name = Name.new("Noel", "Rappin")
 expect(name.sort_name).to eq("Rappin, Noel")
 end
 end
  2. RSpec with Parentheses require 'rails_helper'
 
 RSpec.describe(Name) do
 self.it(“produces a

    sort name”) do
 name = Name.new("Noel", "Rappin")
 self.expect(name.sort_name).to(self.eq(“Rappin, Noel”))
 end
 end
  3. Key RSpec words require 'rails_helper'
 
 RSpec.describe(Name) do
 self.it(“produces a

    sort name”) do
 name = Name.new("Noel", "Rappin")
 self.expect(name.sort_name).to(self.eq(“Rappin, Noel”))
 end
 end
  4. require 'rails_helper'
 
 RSpec.describe(Name) do
 self.it(“produces a sort name”) do


    name = Name.new("Noel", "Rappin")
 self.expect(name.sort_name).to(self.eq(“Rappin, Noel”))
 end
 end
  5. require 'rails_helper'
 
 RSpec.describe(Name) do
 self.it(“produces a sort name”) do


    name = Name.new("Noel", "Rappin")
 self.expect(name.sort_name).to(self.eq(“Rappin, Noel”))
 end
 end Example Group
  6. require 'rails_helper'
 
 RSpec.describe(Name) do
 self.it(“produces a sort name”) do


    name = Name.new("Noel", "Rappin")
 self.expect(name.sort_name).to(self.eq(“Rappin, Noel”))
 end
 end Example Group Example
  7. require 'rails_helper'
 
 RSpec.describe(Name) do
 self.it(“produces a sort name”) do


    name = Name.new("Noel", "Rappin")
 self.expect(name.sort_name).to(self.eq(“Rappin, Noel”))
 end
 end Example Group Example ExpectationTarget
  8. require 'rails_helper'
 
 RSpec.describe(Name) do
 self.it(“produces a sort name”) do


    name = Name.new("Noel", "Rappin")
 self.expect(name.sort_name).to(self.eq(“Rappin, Noel”))
 end
 end Example Group Example ExpectationTarget Matcher
  9. Example Group • Created by describe or context • Arguments

    are a description, metadata, and block • Creates an anonymous subclass of ExampleGroup • Executes the block in the context of the class RSpec.describe Name, metadata: true do
 #stuff
 end
  10. Creating an ExampleGroup def self.subclass(parent, description, args, &example_group_block)
 subclass =

    Class.new(parent)
 subclass.set_it_up(description, *args, &example_group_block)
 subclass.module_exec(&example_group_block) if example_group_block
 MemoizedHelpers.define_helpers_on(subclass)
 subclass
 end
  11. Example Group Subclass def self.subclass(parent, description, args, &example_group_block)
 subclass =

    Class.new(parent)
 subclass.set_it_up(description, *args, &example_group_block)
 subclass.module_exec(&example_group_block) if example_group_block
 MemoizedHelpers.define_helpers_on(subclass)
 subclass
 end
  12. Setup (configure mock package) def self.subclass(parent, description, args, &example_group_block)
 subclass

    = Class.new(parent)
 subclass.set_it_up(description, *args, &example_group_block)
 subclass.module_exec(&example_group_block) if example_group_block
 MemoizedHelpers.define_helpers_on(subclass)
 subclass
 end
  13. Setup (configure mock package) def self.subclass(parent, description, args, &example_group_block)
 subclass

    = Class.new(parent)
 subclass.set_it_up(description, *args, &example_group_block)
 subclass.module_exec(&example_group_block) if example_group_block
 MemoizedHelpers.define_helpers_on(subclass)
 subclass
 end
  14. Setup (configure mock package) def self.subclass(parent, description, args, &example_group_block)
 subclass

    = Class.new(parent)
 subclass.set_it_up(description, *args, &example_group_block)
 subclass.module_exec(&example_group_block) if example_group_block
 MemoizedHelpers.define_helpers_on(subclass)
 subclass
 end
  15. What is module_exec? class Thing
 end
 
 Thing.module_exec(arg) do |arg|


    def hello() "Hello there!" end
 end 
 puts Thing.new.hello()
  16. Minimal RSpec with Parentheses require 'rails_helper'
 
 <<ExampleGroupSubclass>>.module_exec do
 it(“produces

    a sort name”) do
 name = Name.new("Noel", "Rappin")
 expect(name.sort_name).to(eq(“Rappin, Noel”))
 end
 end
  17. Example • Created by it, example, specify — instance methods

    of ExampleGroup • Arguments are description, metadata, and a block • Assigns the example to an array class attribute of the ExampleGroup it(“produces a sort name”) do
 #stuff
 end
  18. Creating an Example desc, *args = *all_args
 options = Metadata.build_hash_from(args)

    unless block
 options.update(:skip => RSpec::Core::Pending::NOT_YET_IMPLEMENTED) end options.update(extra_options)
 examples << RSpec::Core::Example.new(self, desc, options, block)
 examples.last
  19. Creating an Example desc, *args = *all_args
 options = Metadata.build_hash_from(args)

    unless block
 options.update(:skip => RSpec::Core::Pending::NOT_YET_IMPLEMENTED) end options.update(extra_options)
 examples << RSpec::Core::Example.new(self, desc, options, block)
 examples.last
  20. Creating an Example desc, *args = *all_args
 options = Metadata.build_hash_from(args)

    unless block
 options.update(:skip => RSpec::Core::Pending::NOT_YET_IMPLEMENTED) end options.update(extra_options)
 examples << RSpec::Core::Example.new(self, desc, options, block)
 examples.last
  21. What happens at run time? •RSpec creates an instance of

    the anonymous ExampleGroup class •The new instance runs all of its examples
  22. Running an Example Group def self.run(reporter=RSpec::Core::NullReporter)
 return if RSpec.world.wants_to_quit
 reporter.example_group_started(self)


    should_run_context_hooks = descendant_filtered_examples.any?
 begin
 run_before_context_hooks(new('before(:context) hook')) if should_run_context_hooks
 result_for_this_group = run_examples(reporter)
 results_for_descendants = ordering_strategy.order(children).map { |child| child.run(reporter) }.all?
 result_for_this_group && results_for_descendants
 rescue Pending::SkipDeclaredInExample => ex
 for_filtered_examples(reporter) { |example| example.skip_with_exception(reporter, ex) }
 true
 rescue Exception => ex
 RSpec.world.wants_to_quit = true if fail_fast?
 for_filtered_examples(reporter) { |example| example.fail_with_exception(reporter, ex) }
 false
 ensure
 run_after_context_hooks(new('after(:context) hook')) if should_run_context_hooks
 reporter.example_group_finished(self)
 end
 end
  23. Running an Example Group begin
 run_before_context_hooks(new('before(:context) hook')) if should_run_context_hooks
 result_for_this_group

    = run_examples(reporter)
 results_for_descendants = ordering_strategy.order(children).map { |child| child.run(reporter) }.all?
 result_for_this_group && results_for_descendants

  24. Running an Example Group 
 rescue Pending::SkipDeclaredInExample => ex
 for_filtered_examples(reporter)

    { |example| example.skip_with_exception(reporter, ex) }
 true
 rescue Exception => ex
 RSpec.world.wants_to_quit = true if fail_fast?
 for_filtered_examples(reporter) { |example| example.fail_with_exception(reporter, ex) }
 false

  25. Running an Example Group def self.run(reporter=RSpec::Core::NullReporter)
 return if RSpec.world.wants_to_quit
 reporter.example_group_started(self)


    should_run_context_hooks = descendant_filtered_examples.any?
 begin
 run_before_context_hooks(new('before(:context) hook')) if should_run_context_hooks
 result_for_this_group = run_examples(reporter)
 results_for_descendants = ordering_strategy.order(children).map { |child| child.run(reporter) }.all?
 result_for_this_group && results_for_descendants
 rescue Pending::SkipDeclaredInExample => ex
 for_filtered_examples(reporter) { |example| example.skip_with_exception(reporter, ex) }
 true
 rescue Exception => ex
 RSpec.world.wants_to_quit = true if fail_fast?
 for_filtered_examples(reporter) { |example| example.fail_with_exception(reporter, ex) }
 false
 ensure
 run_after_context_hooks(new('after(:context) hook')) if should_run_context_hooks
 reporter.example_group_finished(self)
 end
 end
  26. Running Examples def self.run_examples(reporter)
 ordering_strategy.order(filtered_examples).map do |example|
 next if RSpec.world.wants_to_quit


    instance = new(example.inspect_output)
 set_ivars(instance, before_context_ivars)
 succeeded = example.run(instance, reporter)
 RSpec.world.wants_to_quit = true if fail_fast? && !succeeded
 succeeded
 end.all?
 end
  27. Running Examples def self.run_examples(reporter)
 ordering_strategy.order(filtered_examples).map do |example|
 next if RSpec.world.wants_to_quit


    instance = new(example.inspect_output)
 set_ivars(instance, before_context_ivars)
 succeeded = example.run(instance, reporter)
 RSpec.world.wants_to_quit = true if fail_fast? && !succeeded
 succeeded
 end.all?
 end
  28. Running Examples def self.run_examples(reporter)
 ordering_strategy.order(filtered_examples).map do |example|
 next if RSpec.world.wants_to_quit


    instance = new(example.inspect_output)
 set_ivars(instance, before_context_ivars)
 succeeded = example.run(instance, reporter)
 RSpec.world.wants_to_quit = true if fail_fast? && !succeeded
 succeeded
 end.all?
 end
  29. Running Examples def self.run_examples(reporter)
 ordering_strategy.order(filtered_examples).map do |example|
 next if RSpec.world.wants_to_quit


    instance = new(example.inspect_output)
 set_ivars(instance, before_context_ivars)
 succeeded = example.run(instance, reporter)
 RSpec.world.wants_to_quit = true if fail_fast? && !succeeded
 succeeded
 end.all?
 end
  30. Running Examples def self.run_examples(reporter)
 ordering_strategy.order(filtered_examples).map do |example|
 next if RSpec.world.wants_to_quit


    instance = new(example.inspect_output)
 set_ivars(instance, before_context_ivars)
 succeeded = example.run(instance, reporter)
 RSpec.world.wants_to_quit = true if fail_fast? && !succeeded
 succeeded
 end.all?
 end
  31. Running Examples def self.run_examples(reporter)
 ordering_strategy.order(filtered_examples).map do |example|
 next if RSpec.world.wants_to_quit


    instance = new(example.inspect_output)
 set_ivars(instance, before_context_ivars)
 succeeded = example.run(instance, reporter)
 RSpec.world.wants_to_quit = true if fail_fast? && !succeeded
 succeeded
 end.all?
 end
  32. Running Examples def self.run_examples(reporter)
 ordering_strategy.order(filtered_examples).map do |example|
 next if RSpec.world.wants_to_quit


    instance = new(example.inspect_output)
 set_ivars(instance, before_context_ivars)
 succeeded = example.run(instance, reporter)
 RSpec.world.wants_to_quit = true if fail_fast? && !succeeded
 succeeded
 end.all?
 end
  33. Running One Example • Check that it is not pending

    • Run before blocks • instance_exec the block in the context of the example group • Raise is the example skips or fails • Run after blocks
  34. begin
 run_before_example
 @example_group_instance.instance_exec(self, &@example_block)
 if pending?
 Pending.mark_fixed! self
 raise Pending::PendingExampleFixedError,


    'Expected example to fail since it is pending, but it passed.',
 [location]
 end
 rescue Pending::SkipDeclaredInExample
 # no-op, required metadata has already been set by the `skip`
 # method.
 rescue Exception => e
 set_exception(e)
 ensure
 run_after_example
 finish(reporter)
 end
  35. begin
 run_before_example
 @example_group_instance.instance_exec(self, &@example_block)
 if pending?
 Pending.mark_fixed! self
 raise Pending::PendingExampleFixedError,


    'Expected example to fail since it is pending, but it passed.',
 [location]
 end
 rescue Pending::SkipDeclaredInExample
 # no-op, required metadata has already been set by the `skip`
 # method.
 rescue Exception => e
 set_exception(e)
 ensure
 run_after_example
 finish(reporter)
 end
  36. begin
 run_before_example
 @example_group_instance.instance_exec(self, &@example_block)
 if pending?
 Pending.mark_fixed! self
 raise Pending::PendingExampleFixedError,


    'Expected example to fail since it is pending, but it passed.',
 [location]
 end
 rescue Pending::SkipDeclaredInExample
 # no-op, required metadata has already been set by the `skip`
 # method.
 rescue Exception => e
 set_exception(e)
 ensure
 run_after_example
 finish(reporter)
 end
  37. begin
 run_before_example
 @example_group_instance.instance_exec(self, &@example_block)
 if pending?
 Pending.mark_fixed! self
 raise Pending::PendingExampleFixedError,


    'Expected example to fail since it is pending, but it passed.',
 [location]
 end
 rescue Pending::SkipDeclaredInExample
 # no-op, required metadata has already been set by the `skip`
 # method.
 rescue Exception => e
 set_exception(e)
 ensure
 run_after_example
 finish(reporter)
 end
  38. begin
 run_before_example
 @example_group_instance.instance_exec(self, &@example_block)
 if pending?
 Pending.mark_fixed! self
 raise Pending::PendingExampleFixedError,


    'Expected example to fail since it is pending, but it passed.',
 [location]
 end
 rescue Pending::SkipDeclaredInExample
 # no-op, required metadata has already been set by the `skip`
 # method.
 rescue Exception => e
 set_exception(e)
 ensure
 run_after_example
 finish(reporter)
 end
  39. begin
 run_before_example
 @example_group_instance.instance_exec(self, &@example_block)
 if pending?
 Pending.mark_fixed! self
 raise Pending::PendingExampleFixedError,


    'Expected example to fail since it is pending, but it passed.',
 [location]
 end
 rescue Pending::SkipDeclaredInExample
 # no-op, required metadata has already been set by the `skip`
 # method.
 rescue Exception => e
 set_exception(e)
 ensure
 run_after_example
 finish(reporter)
 end
  40. BE_PREDICATE_REGEX = /^(be_(?:an?_)?)(.*)/
 HAS_REGEX = /^(?:have_)(.*)/
 
 def method_missing(method, *args,

    &block)
 case method.to_s
 when BE_PREDICATE_REGEX
 BuiltIn::BePredicate.new(method, *args, &block)
 when HAS_REGEX
 BuiltIn::Has.new(method, *args, &block)
 else
 super
 end
 end
  41. class BePredicate < BaseMatcher
 include BeHelpers
 
 def initialize(*args, &block)


    @expected = parse_expected(args.shift)
 @args = args
 @block = block
 end
 
 def matches?(actual, &block)
 @actual = actual
 @block ||= block
 predicate_accessible? && predicate_matches?
 end
 
 def predicate_accessible?
 actual.respond_to?(predicate) || actual.respond_to?(present_tense_predicate)
 end
 
 def predicate_matches?
 method_name = actual.respond_to?(predicate) ? predicate : present_tense_predicate
 @predicate_matches = actual.__send__(method_name, *@args, &@block)
 end
 end
  42. 
 
 def matches?(actual, &block)
 @actual = actual
 @block ||=

    block
 predicate_accessible? && predicate_matches?
 end
 
 def predicate_accessible?
 actual.respond_to?(predicate) || actual.respond_to?(present_tense_predicate)
 end
 
 def predicate_matches?
 method_name = actual.respond_to?(predicate) ? predicate : present_tense_predicate
 @predicate_matches = actual.__send__(method_name, *@args, &@block)
 end
  43. 
 
 def matches?(actual, &block)
 @actual = actual
 @block ||=

    block
 predicate_accessible? && predicate_matches?
 end
 
 def predicate_accessible?
 actual.respond_to?(predicate) || actual.respond_to?(present_tense_predicate)
 end
 
 def predicate_matches?
 method_name = actual.respond_to?(predicate) ? predicate : present_tense_predicate
 @predicate_matches = actual.__send__(method_name, *@args, &@block)
 end
  44. 
 
 def matches?(actual, &block)
 @actual = actual
 @block ||=

    block
 predicate_accessible? && predicate_matches?
 end
 
 def predicate_accessible?
 actual.respond_to?(predicate) || actual.respond_to?(present_tense_predicate)
 end
 
 def predicate_matches?
 method_name = actual.respond_to?(predicate) ? predicate : present_tense_predicate
 @predicate_matches = actual.__send__(method_name, *@args, &@block)
 end
  45. 
 
 def matches?(actual, &block)
 @actual = actual
 @block ||=

    block
 predicate_accessible? && predicate_matches?
 end
 
 def predicate_accessible?
 actual.respond_to?(predicate) || actual.respond_to?(present_tense_predicate)
 end
 
 def predicate_matches?
 method_name = actual.respond_to?(predicate) ? predicate : present_tense_predicate
 @predicate_matches = actual.__send__(method_name, *@args, &@block)
 end
  46. define_example_group_method :example_group
 define_example_group_method :describe
 define_example_group_method :context
 define_example_group_method :xdescribe,
 :skip =>

    "Temporarily skipped with xdescribe"
 define_example_group_method :xcontext,
 :skip => "Temporarily skipped with xcontext"
 define_example_group_method :fdescribe,
 :focus => true
 define_example_group_method :fcontext,
 :focus => true
  47. define_example_method :specify
 define_example_method :focus, :focus => true
 define_example_method :fexample, :focus

    => true
 define_example_method :fit, :focus => true
 define_example_method :fspecify, :focus => true
 define_example_method :xexample, :skip => 'Temporarily skipped with xexample'
 define_example_method :xit, :skip => 'Temporarily skipped with xit'
 define_example_method :skip, :skip => true
 define_example_method :pending, :pending => true
  48. require 'rails_helper'
 
 module RSpec
 module Core
 class ExampleGroup
 define_example_group_method

    :!
 define_example_method :" end
 end
 
 module Matchers
 def #(value=::RSpec::Expectations::ExpectationTarget::UndefinedValue, &block)
 ::RSpec::Expectations::ExpectationTarget.for(value, block)
 end
 alias_matcher :❤, :eq
 end
 end
  49. Simple Mock Example require 'rails_helper'
 
 RSpec.describe Name do
 it

    "assigns a rating category" do
 user = User.new()
 expect(user).to receive(:credit_rating).and_return(1000)
 expect(user.rating_category).to eq("approved")
 end
 end
  50. What would this mock need to do? •Ensure the method

    is not called •Track how often it’s called •Verify that expectations are met
  51. Ruby Method Lookup path • Singleton class • instance methods

    of class • Included modules • parent of class • parent’s included modules • and so on…
  52. Singleton Class class << x def foo #something end end

    def x.bar #something end x.foo; x.bar
  53. Key Concepts • Receive is a matcher • Space •

    Proxy • Method Double Original Object Singleton Class MethodDouble Proxy Space Receive Matcher
  54. Inside the Receive Matcher def setup_expectation(subject, &block)
 warn_if_any_instance("expect", subject)
 @describable

    = setup_mock_proxy_method_substitute( subject, :add_message_expectation, block)
 end
 alias matches? setup_expectation
 
 def setup_mock_proxy_method_substitute(subject, method, block)
 proxy = ::RSpec::Mocks.space.proxy_for(subject)
 setup_method_substitute(proxy, method, block)
 end
  55. Inside the Receive Matcher def setup_expectation(subject, &block)
 warn_if_any_instance("expect", subject)
 @describable

    = setup_mock_proxy_method_substitute( subject, :add_message_expectation, block)
 end
 alias matches? setup_expectation
 
 def setup_mock_proxy_method_substitute(subject, method, block)
 proxy = ::RSpec::Mocks.space.proxy_for(subject)
 setup_method_substitute(proxy, method, block)
 end
  56. Inside the Receive Matcher def setup_expectation(subject, &block)
 warn_if_any_instance("expect", subject)
 @describable

    = setup_mock_proxy_method_substitute( subject, :add_message_expectation, block)
 end
 alias matches? setup_expectation
 
 def setup_mock_proxy_method_substitute(subject, method, block)
 proxy = ::RSpec::Mocks.space.proxy_for(subject)
 setup_method_substitute(proxy, method, block)
 end
  57. Inside the Receive Matcher def setup_expectation(subject, &block)
 warn_if_any_instance("expect", subject)
 @describable

    = setup_mock_proxy_method_substitute( subject, :add_message_expectation, block)
 end
 alias matches? setup_expectation
 
 def setup_mock_proxy_method_substitute(subject, method, block)
 proxy = ::RSpec::Mocks.space.proxy_for(subject)
 setup_method_substitute(proxy, method, block)
 end
  58. Proxy adds Expectations def add_message_expectation(method_name, opts={}, &block)
 location = opts.fetch(:expected_from)

    { CallerFilter.first_non_rspec_line }
 meth_double = method_double_for(method_name)
 
 if null_object? && !block
 meth_double.add_default_stub(@error_generator, @order_group, location, opts) do
 @object
 end
 end
 
 meth_double.add_expectation @error_generator, @order_group, location, opts, &block
 end
  59. Proxy adds Expectations def add_message_expectation(method_name, opts={}, &block)
 location = opts.fetch(:expected_from)

    { CallerFilter.first_non_rspec_line }
 meth_double = method_double_for(method_name)
 
 if null_object? && !block
 meth_double.add_default_stub(@error_generator, @order_group, location, opts) do
 @object
 end
 end
 
 meth_double.add_expectation @error_generator, @order_group, location, opts, &block
 end
  60. Proxy adds Expectations def add_message_expectation(method_name, opts={}, &block)
 location = opts.fetch(:expected_from)

    { CallerFilter.first_non_rspec_line }
 meth_double = method_double_for(method_name)
 
 if null_object? && !block
 meth_double.add_default_stub(@error_generator, @order_group, location, opts) do
 @object
 end
 end
 
 meth_double.add_expectation @error_generator, @order_group, location, opts, &block
 end
  61. Method Double def define_proxy_method
 return if @method_is_proxied
 save_original_method!
 definition_target.class_exec( self,

    method_name, visibility) do |method_double, method_name, visibility|
 define_method(method_name) do |*args, &block|
 method_double.proxy_method_invoked(self, *args, &block)
 end
 __send__(visibility, method_name)
 end
 @method_is_proxied = true
 end
 
 def proxy_method_invoked(_obj, *args, &block)
 @proxy.message_received method_name, *args, &block
 end
  62. Method Double def define_proxy_method
 return if @method_is_proxied
 save_original_method!
 definition_target.class_exec( self,

    method_name, visibility) do |method_double, method_name, visibility|
 define_method(method_name) do |*args, &block|
 method_double.proxy_method_invoked(self, *args, &block)
 end
 __send__(visibility, method_name)
 end
 @method_is_proxied = true
 end
 
 def proxy_method_invoked(_obj, *args, &block)
 @proxy.message_received method_name, *args, &block
 end
  63. Method Double def define_proxy_method
 return if @method_is_proxied
 save_original_method!
 definition_target.class_exec( self,

    method_name, visibility) do |method_double, method_name, visibility|
 define_method(method_name) do |*args, &block|
 method_double.proxy_method_invoked(self, *args, &block)
 end
 __send__(visibility, method_name)
 end
 @method_is_proxied = true
 end
 
 def proxy_method_invoked(_obj, *args, &block)
 @proxy.message_received method_name, *args, &block
 end
  64. Method Double def define_proxy_method
 return if @method_is_proxied
 save_original_method!
 definition_target.class_exec( self,

    method_name, visibility) do |method_double, method_name, visibility|
 define_method(method_name) do |*args, &block|
 method_double.proxy_method_invoked(self, *args, &block)
 end
 __send__(visibility, method_name)
 end
 @method_is_proxied = true
 end
 
 def proxy_method_invoked(_obj, *args, &block)
 @proxy.message_received method_name, *args, &block
 end
  65. Method Double def define_proxy_method
 return if @method_is_proxied
 save_original_method!
 definition_target.class_exec( self,

    method_name, visibility) do |method_double, method_name, visibility|
 define_method(method_name) do |*args, &block|
 method_double.proxy_method_invoked(self, *args, &block)
 end
 __send__(visibility, method_name)
 end
 @method_is_proxied = true
 end
 
 def proxy_method_invoked(_obj, *args, &block)
 @proxy.message_received method_name, *args, &block
 end
  66. Noel Rappin Table XI @noelrap http://www.pragprog.com/book/nrtest2 (test2rappin for 25% off)

    http://www.noelrappin.com/trdd http://www.noelrappin.com/mstwjs