rspec 3.0: under the covers
DESCRIPTION
Slides for a code reading of RSpec 3.0, detailing how the RSpec team eliminated monkey patching.TRANSCRIPT
RSpec 3.0: Under the CoversAchieving “Zero Monkey-Patching Mode”
Brian Gesiak
March 13th, 2014
Research Student, The University of Tokyo
@modocache
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
Monkey PatchingHow 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 PatchingHow 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 PatchingHow 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
Monkey Patching Root ObjectsGo 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
Monkey Patching Root ObjectsGo 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
Monkey Patching Root ObjectsGo 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
Monkey Patching Root ObjectsGo 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
Monkey Patching Root ObjectsGo 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
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
RSpec 3.0Achieving “Zero Monkey-Patching Mode”
$ rspec meetup_spec.rb
The rspec Executablerspec-core/exe/rspec
#!/usr/bin/env ruby !require 'rspec/core' RSpec::Core::Runner.invoke
The rspec Executablerspec-core/exe/rspec
#!/usr/bin/env ruby !require 'rspec/core' RSpec::Core::Runner.invoke
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
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
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
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
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
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
Loading Spec FilesRSpec::Core::Configuration.load_spec_files
def load_spec_files files_to_run.uniq.each { |f| load f } @spec_files_loaded = true end
Loading Spec FilesRSpec::Core::Configuration.load_spec_files
def load_spec_files files_to_run.uniq.each { |f| load f } @spec_files_loaded = true end
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
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
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
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
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
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
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
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
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
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
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
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
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
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
Building Example GroupsHierarchies of Example Groups and Examplesclass 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
Building Example GroupsHierarchies of Example Groups and Examplesclass 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
Building Example GroupsHierarchies of Example Groups and Examplesclass 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
Building Example GroupsHierarchies of Example Groups and Examplesclass 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
Building Example GroupsHierarchies of Example Groups and Examples
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
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
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
Running the ExamplesRSpec::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
Running the ExamplesRSpec::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
Running the ExamplesRSpec::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
Running the ExamplesRSpec::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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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 endexpect(meetup).to be_hoppin
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 endexpect(meetup).to be_hoppin
should Emits Deprecation Warning as of RSpec 3.0
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
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
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
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
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
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