rspec 3.0: under the covers

75
RSpec 3.0: Under the Covers Achieving “Zero Monkey-Patching Mode” Brian Gesiak March 13th, 2014 Research Student, The University of Tokyo @modocache

Upload: brian-gesiak

Post on 06-May-2015

8.527 views

Category:

Technology


1 download

DESCRIPTION

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

TRANSCRIPT

Page 1: RSpec 3.0: Under the Covers

RSpec 3.0: Under the CoversAchieving “Zero Monkey-Patching Mode”

Brian Gesiak

March 13th, 2014

Research Student, The University of Tokyo

@modocache

Page 2: RSpec 3.0: Under the Covers

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

Page 3: RSpec 3.0: Under the Covers

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

Page 4: RSpec 3.0: Under the Covers

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

Page 5: RSpec 3.0: Under the Covers

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

Page 6: RSpec 3.0: Under the Covers

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

Page 7: RSpec 3.0: Under the Covers

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

Page 8: RSpec 3.0: Under the Covers

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

Page 9: RSpec 3.0: Under the Covers

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

Page 10: RSpec 3.0: Under the Covers

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

Page 11: RSpec 3.0: Under the Covers

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

Page 12: RSpec 3.0: Under the Covers

RSpec 3.0Achieving “Zero Monkey-Patching Mode”

$ rspec meetup_spec.rb

Page 13: RSpec 3.0: Under the Covers

The rspec Executablerspec-core/exe/rspec

#!/usr/bin/env ruby !require 'rspec/core' RSpec::Core::Runner.invoke

Page 14: RSpec 3.0: Under the Covers

The rspec Executablerspec-core/exe/rspec

#!/usr/bin/env ruby !require 'rspec/core' RSpec::Core::Runner.invoke

Page 15: RSpec 3.0: Under the Covers

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

Page 16: RSpec 3.0: Under the Covers

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

Page 17: RSpec 3.0: Under the Covers

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

Page 18: RSpec 3.0: Under the Covers

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

Page 19: RSpec 3.0: Under the Covers

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

Page 20: RSpec 3.0: Under the Covers

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

Page 21: RSpec 3.0: Under the Covers

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

Page 22: RSpec 3.0: Under the Covers

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

Page 23: RSpec 3.0: Under the Covers

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

Page 24: RSpec 3.0: Under the Covers

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

Page 25: RSpec 3.0: Under the Covers

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

Page 26: RSpec 3.0: Under the Covers

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

Page 27: RSpec 3.0: Under the Covers

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

Page 28: RSpec 3.0: Under the Covers

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

Page 29: RSpec 3.0: Under the Covers

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

Page 30: RSpec 3.0: Under the Covers

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

Page 31: RSpec 3.0: Under the Covers

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

Page 32: RSpec 3.0: Under the Covers

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

Page 33: RSpec 3.0: Under the Covers

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

Page 34: RSpec 3.0: Under the Covers

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

Page 35: RSpec 3.0: Under the Covers

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

Page 36: RSpec 3.0: Under the Covers

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

Page 37: RSpec 3.0: Under the Covers

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

Page 38: RSpec 3.0: Under the Covers

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

Page 39: RSpec 3.0: Under the Covers

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

Page 40: RSpec 3.0: Under the Covers

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

Page 41: RSpec 3.0: Under the Covers

Building Example GroupsHierarchies of Example Groups and Examples

Page 42: RSpec 3.0: Under the Covers

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

Page 43: RSpec 3.0: Under the Covers

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

Page 44: RSpec 3.0: Under the Covers

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

Page 45: RSpec 3.0: Under the Covers

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

Page 46: RSpec 3.0: Under the Covers

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

Page 47: RSpec 3.0: Under the Covers

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

Page 48: RSpec 3.0: Under the Covers

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

Page 49: RSpec 3.0: Under the Covers

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

Page 50: RSpec 3.0: Under the Covers

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

Page 51: RSpec 3.0: Under the Covers

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

Page 52: RSpec 3.0: Under the Covers

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

Page 53: RSpec 3.0: Under the Covers

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

Page 54: RSpec 3.0: Under the Covers

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

Page 55: RSpec 3.0: Under the Covers

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

Page 56: RSpec 3.0: Under the Covers

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

Page 57: RSpec 3.0: Under the Covers

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

Page 58: RSpec 3.0: Under the Covers

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

Page 59: RSpec 3.0: Under the Covers

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

Page 60: RSpec 3.0: Under the Covers

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

Page 61: RSpec 3.0: Under the Covers

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

Page 62: RSpec 3.0: Under the Covers

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

Page 63: RSpec 3.0: Under the Covers

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

Page 64: RSpec 3.0: Under the Covers

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

Page 65: RSpec 3.0: Under the Covers

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

Page 66: RSpec 3.0: Under the Covers

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

Page 67: RSpec 3.0: Under the Covers

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

Page 68: RSpec 3.0: Under the Covers

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

Page 69: RSpec 3.0: Under the Covers

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

Page 70: RSpec 3.0: Under the Covers

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

Page 71: RSpec 3.0: Under the Covers

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

Page 72: RSpec 3.0: Under the Covers

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

Page 73: RSpec 3.0: Under the Covers

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

Page 74: RSpec 3.0: Under the Covers

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

Page 75: RSpec 3.0: Under the Covers

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