RSpec 3.0: Under the Covers

9,254 views
9,270 views

Published on

Slides for a code reading of RSpec 3.0, detailing how the RSpec team eliminated monkey patching.

Published in: Technology
0 Comments
0 Likes
Statistics
Notes
  • Be the first to comment

  • Be the first to like this

No Downloads
Views
Total views
9,254
On SlideShare
0
From Embeds
0
Number of Embeds
7
Actions
Shares
0
Downloads
14
Comments
0
Likes
0
Embeds 0
No embeds

No notes for slide

RSpec 3.0: Under the Covers

  1. 1. RSpec 3.0: Under the Covers Achieving“Zero Monkey-Patching Mode” Brian Gesiak March 13th, 2014 Research Student, The University of Tokyo @modocache
  2. 2. Today • Monkey patching • How does RSpec work? • The rspec executable • Loading spec files • Example groups: describe and context • RSpec 2.11: describe no longer added to every Object • Running examples (it blocks) • Expectations • RSpec 2.11: expect-based syntax removes need for adding should to every Object
  3. 3. Monkey Patching How to Do It and Why You Shouldn’t class Array def sum # Also defined in `activesupport`! inject { |sum, x| sum + x } end end ! expect([1, 2, 3].sum).to eq 6
  4. 4. Monkey Patching How to Do It and Why You Shouldn’t class Array def sum # Also defined in `activesupport`! inject { |sum, x| sum + x } end end ! expect([1, 2, 3].sum).to eq 6
  5. 5. Monkey Patching How to Do It and Why You Shouldn’t class Array def sum # Also defined in `activesupport`! inject { |sum, x| sum + x } end end ! expect([1, 2, 3].sum).to eq 6 Monkey patching can lead to cryptic errors
  6. 6. Monkey Patching Root Objects Go Big or Go Home class Object def should # ... end end module Kernel def describe # ... end end # Global method describe # All objects respond # to method Object.new.should
  7. 7. Monkey Patching Root Objects Go Big or Go Home class Object def should # ... end end module Kernel def describe # ... end end # Global method describe # All objects respond # to method Object.new.should
  8. 8. Monkey Patching Root Objects Go Big or Go Home class Object def should # ... end end module Kernel def describe # ... end end # Global method describe # All objects respond # to method Object.new.should
  9. 9. Monkey Patching Root Objects Go Big or Go Home class Object def should # ... end end module Kernel def describe # ... end end # Global method describe # All objects respond # to method Object.new.should
  10. 10. Monkey Patching Root Objects Go Big or Go Home class Object def should # ... end end module Kernel def describe # ... end end # Global method describe # All objects respond # to method Object.new.should
  11. 11. RSpec 3.0 Historically, RSpec has extensively used monkey patching to create its readable syntax, adding methods…to every object. ! In the last few 2.x releases, we’ve worked towards reducing the amount of monkey patching done by RSpec. Zero Monkey-Patching Mode Myron Marston, RSpec Core Member @myronmarston
  12. 12. RSpec 3.0 Achieving“Zero Monkey-Patching Mode” $ rspec meetup_spec.rb
  13. 13. The rspec Executable rspec-core/exe/rspec #!/usr/bin/env ruby ! require 'rspec/core' RSpec::Core::Runner.invoke
  14. 14. The rspec Executable rspec-core/exe/rspec #!/usr/bin/env ruby ! require 'rspec/core' RSpec::Core::Runner.invoke
  15. 15. Loading Spec Files class CommandLine def run(err, out) # ... @configuration.load_spec_files # ... begin @configuration.hooks.run(:before, :suite) @world.ordered_example_groups.map { |g| g.run(reporter) } # ... ensure @configuration.hooks.run(:after, :suite) end end end RSpec::Core::CommandLine.run
  16. 16. Loading Spec Files class CommandLine def run(err, out) # ... @configuration.load_spec_files # ... begin @configuration.hooks.run(:before, :suite) @world.ordered_example_groups.map { |g| g.run(reporter) } # ... ensure @configuration.hooks.run(:after, :suite) end end end RSpec::Core::CommandLine.run
  17. 17. Loading Spec Files class CommandLine def run(err, out) # ... @configuration.load_spec_files # ... begin @configuration.hooks.run(:before, :suite) @world.ordered_example_groups.map { |g| g.run(reporter) } # ... ensure @configuration.hooks.run(:after, :suite) end end end RSpec::Core::CommandLine.run
  18. 18. Loading Spec Files class CommandLine def run(err, out) # ... @configuration.load_spec_files # ... begin @configuration.hooks.run(:before, :suite) @world.ordered_example_groups.map { |g| g.run(reporter) } # ... ensure @configuration.hooks.run(:after, :suite) end end end RSpec::Core::CommandLine.run
  19. 19. Loading Spec Files class CommandLine def run(err, out) # ... @configuration.load_spec_files # ... begin @configuration.hooks.run(:before, :suite) @world.ordered_example_groups.map { |g| g.run(reporter) } # ... ensure @configuration.hooks.run(:after, :suite) end end end RSpec::Core::CommandLine.run
  20. 20. Loading Spec Files class CommandLine def run(err, out) # ... @configuration.load_spec_files # ... begin @configuration.hooks.run(:before, :suite) @world.ordered_example_groups.map { |g| g.run(reporter) } # ... ensure @configuration.hooks.run(:after, :suite) end end end RSpec::Core::CommandLine.run
  21. 21. Loading Spec Files RSpec::Core::Configuration.load_spec_files def load_spec_files files_to_run.uniq.each { |f| load f } @spec_files_loaded = true end
  22. 22. Loading Spec Files RSpec::Core::Configuration.load_spec_files def load_spec_files files_to_run.uniq.each { |f| load f } @spec_files_loaded = true end
  23. 23. Building Example Groups describe TokyoRailsMeetup do let(:meetup) { described_class.new } describe '#start' do context 'without a chef' do it 'gets everyone drunk' do meetup.start expect(meetup.everyone_wasted?) .to be_true end end end end Loading a Typical Spec File
  24. 24. Building Example Groups describe TokyoRailsMeetup do let(:meetup) { described_class.new } describe '#start' do context 'without a chef' do it 'gets everyone drunk' do meetup.start expect(meetup.everyone_wasted?) .to be_true end end end end Loading a Typical Spec File
  25. 25. Where Does describe Come From? RSpec::Core::DSL def self.expose_example_group_alias(name) example_group_aliases << name ! (class << RSpec; self; end). __send__(:define_method, name) # ... ! expose_example_group_alias_globally(name) if exposed_globally? # defines on Module end
  26. 26. Where Does describe Come From? RSpec::Core::DSL def self.expose_example_group_alias(name) example_group_aliases << name ! (class << RSpec; self; end). __send__(:define_method, name) # ... ! expose_example_group_alias_globally(name) if exposed_globally? # defines on Module end
  27. 27. Where Does describe Come From? RSpec::Core::DSL def self.expose_example_group_alias(name) example_group_aliases << name ! (class << RSpec; self; end). __send__(:define_method, name) # ... ! expose_example_group_alias_globally(name) if exposed_globally? # defines on Module end
  28. 28. Where Does describe Come From? RSpec::Core::DSL def self.expose_example_group_alias(name) example_group_aliases << name ! (class << RSpec; self; end). __send__(:define_method, name) # ... ! expose_example_group_alias_globally(name) if exposed_globally? # defines on Module end
  29. 29. Disabling Module Monkey Patching RSpec.configure do |config| config.expose_dsl_globally = false end ! RSpec.describe TokyoRailsMeetup do let(:meetup) { described_class.new } describe '#start' do # ... end end Only Top Level describe Blocks Are Affected
  30. 30. Disabling Module Monkey Patching RSpec.configure do |config| config.expose_dsl_globally = false end ! RSpec.describe TokyoRailsMeetup do let(:meetup) { described_class.new } describe '#start' do # ... end end Only Top Level describe Blocks Are Affected
  31. 31. Disabling Module Monkey Patching RSpec.configure do |config| config.expose_dsl_globally = false end ! RSpec.describe TokyoRailsMeetup do let(:meetup) { described_class.new } describe '#start' do # ... end end Only Top Level describe Blocks Are Affected
  32. 32. Disabling Module Monkey Patching RSpec.configure do |config| config.expose_dsl_globally = false end ! RSpec.describe TokyoRailsMeetup do let(:meetup) { described_class.new } describe '#start' do # ... end end Only Top Level describe Blocks Are Affected Opt-in as of RSpec 2.11
  33. 33. Building Example Groups describe TokyoRailsMeetup do let(:meetup) { described_class.new } describe '#start' do context 'without a chef' do it 'gets everyone drunk' do meetup.start expect(meetup.everyone_wasted?) .to be_true end end end end Loading a Typical Spec File
  34. 34. Building Example Groups describe TokyoRailsMeetup do let(:meetup) { described_class.new } describe '#start' do context 'without a chef' do it 'gets everyone drunk' do meetup.start expect(meetup.everyone_wasted?) .to be_true end end end end Loading a Typical Spec File
  35. 35. Building Example Groups describe TokyoRailsMeetup do let(:meetup) { described_class.new } describe '#start' do context 'without a chef' do it 'gets everyone drunk' do meetup.start expect(meetup.everyone_wasted?) .to be_true end end end end Loading a Typical Spec File
  36. 36. Building Example Groups describe TokyoRailsMeetup do let(:meetup) { described_class.new } describe '#start' do context 'without a chef' do it 'gets everyone drunk' do meetup.start expect(meetup.everyone_wasted?) .to be_true end end end end Loading a Typical Spec File
  37. 37. Building Example Groups Hierarchies of Example Groups and Examples class ExampleGroup def self.example_group(*args, &example_group_block) # ... child = subclass(self, args, &example_group_block) children << child child end ! def self.examples @examples ||= [] end # ... end
  38. 38. Building Example Groups Hierarchies of Example Groups and Examples class ExampleGroup def self.example_group(*args, &example_group_block) # ... child = subclass(self, args, &example_group_block) children << child child end ! def self.examples @examples ||= [] end # ... end
  39. 39. Building Example Groups Hierarchies of Example Groups and Examples class ExampleGroup def self.example_group(*args, &example_group_block) # ... child = subclass(self, args, &example_group_block) children << child child end ! def self.examples @examples ||= [] end # ... end
  40. 40. Building Example Groups Hierarchies of Example Groups and Examples class ExampleGroup def self.example_group(*args, &example_group_block) # ... child = subclass(self, args, &example_group_block) children << child child end ! def self.examples @examples ||= [] end # ... end
  41. 41. Building Example Groups Hierarchies of Example Groups and Examples
  42. 42. Running the Examples class CommandLine def run(err, out) # ... @configuration.load_spec_files # ... begin @configuration.hooks.run(:before, :suite) @world.ordered_example_groups.map { |g| g.run(reporter) } # ... ensure @configuration.hooks.run(:after, :suite) end end end RSpec::Core::CommandLine.run
  43. 43. Running the Examples class CommandLine def run(err, out) # ... @configuration.load_spec_files # ... begin @configuration.hooks.run(:before, :suite) @world.ordered_example_groups.map { |g| g.run(reporter) } # ... ensure @configuration.hooks.run(:after, :suite) end end end RSpec::Core::CommandLine.run
  44. 44. Running the Examples class CommandLine def run(err, out) # ... @configuration.load_spec_files # ... begin @configuration.hooks.run(:before, :suite) @world.ordered_example_groups.map { |g| g.run(reporter) } # ... ensure @configuration.hooks.run(:after, :suite) end end end RSpec::Core::CommandLine.run
  45. 45. Running the Examples RSpec::Core::Example.run def run(example_group_instance, reporter) # ... begin run_before_each @example_group_instance. instance_exec(self, &@example_block) rescue Exception => e set_exception(e) ensure run_after_each end end
  46. 46. Running the Examples RSpec::Core::Example.run def run(example_group_instance, reporter) # ... begin run_before_each @example_group_instance. instance_exec(self, &@example_block) rescue Exception => e set_exception(e) ensure run_after_each end end
  47. 47. Running the Examples RSpec::Core::Example.run def run(example_group_instance, reporter) # ... begin run_before_each @example_group_instance. instance_exec(self, &@example_block) rescue Exception => e set_exception(e) ensure run_after_each end end
  48. 48. Running the Examples RSpec::Core::Example.run def run(example_group_instance, reporter) # ... begin run_before_each @example_group_instance. instance_exec(self, &@example_block) rescue Exception => e set_exception(e) ensure run_after_each end end
  49. 49. Making Expectations it 'is a grand old time' do meetup.start meetup.should be_hoppin end ! it 'gets everyone drunk' do meetup.start expect(meetup.everyone_wasted?).to be_true end The Deprecated should Syntax
  50. 50. Making Expectations it 'is a grand old time' do meetup.start meetup.should be_hoppin end ! it 'gets everyone drunk' do meetup.start expect(meetup.everyone_wasted?).to be_true end The Deprecated should Syntax
  51. 51. How should is Monkey Patched class Configuration # ... def syntax=(values) if Array(values).include?(:expect) Expectations::Syntax.enable_expect else Expectations::Syntax.disable_expect end ! if Array(values).include?(:should) Expectations::Syntax.enable_should else Expectations::Syntax.disable_should end end end RSpec::Expectations::Configuration
  52. 52. How should is Monkey Patched class Configuration # ... def syntax=(values) if Array(values).include?(:expect) Expectations::Syntax.enable_expect else Expectations::Syntax.disable_expect end ! if Array(values).include?(:should) Expectations::Syntax.enable_should else Expectations::Syntax.disable_should end end end RSpec::Expectations::Configuration
  53. 53. How should is Monkey Patched class Configuration # ... def syntax=(values) if Array(values).include?(:expect) Expectations::Syntax.enable_expect else Expectations::Syntax.disable_expect end ! if Array(values).include?(:should) Expectations::Syntax.enable_should else Expectations::Syntax.disable_should end end end RSpec::Expectations::Configuration
  54. 54. How should is Monkey Patched class Configuration # ... def syntax=(values) if Array(values).include?(:expect) Expectations::Syntax.enable_expect else Expectations::Syntax.disable_expect end ! if Array(values).include?(:should) Expectations::Syntax.enable_should else Expectations::Syntax.disable_should end end end RSpec::Expectations::Configuration
  55. 55. How should is Monkey Patched def enable_should(syntax_host=::Object.ancestors.last) # ... syntax_host.module_exec do def should(matcher=nil, message=nil, &block) # ... end ! def should_not(matcher=nil, message=nil, &block) # ... end end end RSpec::Expectations::Syntax
  56. 56. How should is Monkey Patched def enable_should(syntax_host=::Object.ancestors.last) # ... syntax_host.module_exec do def should(matcher=nil, message=nil, &block) # ... end ! def should_not(matcher=nil, message=nil, &block) # ... end end end RSpec::Expectations::Syntax
  57. 57. How should is Monkey Patched def enable_should(syntax_host=::Object.ancestors.last) # ... syntax_host.module_exec do def should(matcher=nil, message=nil, &block) # ... end ! def should_not(matcher=nil, message=nil, &block) # ... end end end RSpec::Expectations::Syntax
  58. 58. Making Expectations it 'is a grand old time' do meetup.start meetup.should be_hoppin end ! it 'gets everyone drunk' do meetup.start expect(meetup.everyone_wasted?).to be_true end The New expect(…).to Syntax
  59. 59. Making Expectations it 'is a grand old time' do meetup.start meetup.should be_hoppin end ! it 'gets everyone drunk' do meetup.start expect(meetup.everyone_wasted?).to be_true end The New expect(…).to Syntax
  60. 60. Making Expectations it 'is a grand old time' do meetup.start meetup.should be_hoppin end ! it 'gets everyone drunk' do meetup.start expect(meetup.everyone_wasted?).to be_true end The New expect(…).to Syntax
  61. 61. How expect is Implemented def enable_expect(syntax_host=::RSpec::Matchers) # ... syntax_host.module_exec do def expect(*target, &target_block) # ... end end end RSpec::Expectations::Syntax
  62. 62. How expect is Implemented def enable_expect(syntax_host=::RSpec::Matchers) # ... syntax_host.module_exec do def expect(*target, &target_block) # ... end end end RSpec::Expectations::Syntax
  63. 63. How expect is Implemented def enable_expect(syntax_host=::RSpec::Matchers) # ... syntax_host.module_exec do def expect(*target, &target_block) # ... end end end RSpec::Expectations::Syntax
  64. 64. Disabling should Monkey Patching RSpec.configure do |config| config.expect_with :rspec do |c| c.syntax = [:should, :expect] c.syntax = :expect end end ! it 'is a grand old time' do meetup.start meetup.should be_hoppin end
  65. 65. Disabling should Monkey Patching RSpec.configure do |config| config.expect_with :rspec do |c| c.syntax = [:should, :expect] c.syntax = :expect end end ! it 'is a grand old time' do meetup.start meetup.should be_hoppin end
  66. 66. Disabling should Monkey Patching RSpec.configure do |config| config.expect_with :rspec do |c| c.syntax = [:should, :expect] c.syntax = :expect end end ! it 'is a grand old time' do meetup.start meetup.should be_hoppin end
  67. 67. Disabling should Monkey Patching RSpec.configure do |config| config.expect_with :rspec do |c| c.syntax = [:should, :expect] c.syntax = :expect end end ! it 'is a grand old time' do meetup.start meetup.should be_hoppin end
  68. 68. Disabling should Monkey Patching RSpec.configure do |config| config.expect_with :rspec do |c| c.syntax = [:should, :expect] c.syntax = :expect end end ! it 'is a grand old time' do meetup.start meetup.should be_hoppin end expect(meetup).to be_hoppin
  69. 69. Disabling should Monkey Patching RSpec.configure do |config| config.expect_with :rspec do |c| c.syntax = [:should, :expect] c.syntax = :expect end end ! it 'is a grand old time' do meetup.start meetup.should be_hoppin end expect(meetup).to be_hoppin should Emits Deprecation Warning as of RSpec 3.0
  70. 70. Bringing it All Together class CommandLine def run(err, out) # ... @configuration.load_spec_files # ... begin @configuration.hooks.run(:before, :suite) @world.ordered_example_groups.map { |g| g.run(reporter) } # ... ensure @configuration.hooks.run(:after, :suite) end end end Load the Specs & Build Example Groups, then Run
  71. 71. Bringing it All Together class CommandLine def run(err, out) # ... @configuration.load_spec_files # ... begin @configuration.hooks.run(:before, :suite) @world.ordered_example_groups.map { |g| g.run(reporter) } # ... ensure @configuration.hooks.run(:after, :suite) end end end Load the Specs & Build Example Groups, then Run
  72. 72. Bringing it All Together class CommandLine def run(err, out) # ... @configuration.load_spec_files # ... begin @configuration.hooks.run(:before, :suite) @world.ordered_example_groups.map { |g| g.run(reporter) } # ... ensure @configuration.hooks.run(:after, :suite) end end end Load the Specs & Build Example Groups, then Run
  73. 73. Bringing it All Together class CommandLine def run(err, out) # ... @configuration.load_spec_files # ... begin @configuration.hooks.run(:before, :suite) @world.ordered_example_groups.map { |g| g.run(reporter) } # ... ensure @configuration.hooks.run(:after, :suite) end end end Load the Specs & Build Example Groups, then Run
  74. 74. Bringing it All Together class CommandLine def run(err, out) # ... @configuration.load_spec_files # ... begin @configuration.hooks.run(:before, :suite) @world.ordered_example_groups.map { |g| g.run(reporter) } # ... ensure @configuration.hooks.run(:after, :suite) end end end Load the Specs & Build Example Groups, then Run
  75. 75. Want to Learn More about RSpec? • http://modocache.io/rspec-under-the-covers • Expectations in RSpec 3.0 • RSpec Output Formatting • Shared Examples in RSpec ! • Follow me on Twitter and GitHub at @modocache • First person to tweet me gets an Atom invite! #swag ! • Myron Marston’s blog: http://myronmars.to/n/dev-blog

×