The document discusses how RSpec achieves "zero monkey patching mode" in version 3.0. It describes how RSpec previously extensively used monkey patching but has worked to reduce this in recent versions. It details how RSpec loads spec files, builds example groups and hierarchies, runs examples, and makes expectations without monkey patching core objects or classes. Key aspects covered include the rspec executable, loading files, building example groups, running examples, and the expect-based syntax.
WordPress Websites for Engineers: Elevate Your Brand
RSpec 3.0: Under the Covers
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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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
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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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
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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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