To introduce myself, my name is Sam Phippen.
- [Audience member] Hi, Sam.
- Hi, Alfie, hi people.
I'm samphippen on Twitter, samphippen on GitHub.
You can take a look at my profiles there
if you're interested in finding out more about me.
If you do take a look at my GitHub profile,
you'll find that I spend most of my time on GitHub
committing as a member of the RSpec Core team.
That's one of the reasons why I'm here, today.
I think it's really important that RSpec is well represented
in community events, like this,
and people who use the framework everyday
get the opportunity to talk someone
who works on the framework full-time.
So, if you have any questions or feedback
about RSpec, if you're loving it,
if you're hating it, please do, let me know
because, like, person-to-person feedback
is one of the ways we influence the future of the framework.
We, actually, just dropped a new release
of RSpec, 3.4, this week,
and it contains a couple of really powerful new features.
So, that might be worth checking out.
I work for a company called Fun and Plausible Solutions.
We're a consultancy based in the U.K.
We tend to work on helping our clients
with their Rails applications, do performance optimizations,
data base analysis and stuff like that.
If that's of any interest to you,
I love coming over here, to do work, as well,
so, maybe, we can find a way to work, together.
Before I get too far into the talk,
I wanted to just, sort of, take a sample of the conference.
Who's enjoying themselves, so far? (audience cheering)
Everyone having a great time?
Great, so, I have a, slightly, more pertinant question
than just getting you all to clap, cheer and applaud.
Which is, who is here for their first time?
Who hasn't been to a RubyConf or a RailsConf, before?
So, that's like the vast majority of you,
and given that you're all having fun
and you're all enjoying the conference,
I thought, that I'd just give a shout out
to the Scholarship and Guiding program.
Which is something I've participated in, now,
for RailsConf, this year, and this conference, as well.
I think the Scholarship and Guiding program
is an excellent feature of these conferences.
It encourages new people,
like the vast majority of people in this room,
to come into our community, to be greeted
and have a really, really great time.
So, if you're enjoying yourself,
and you've enjoyed this conference, a lot,
when it comes time for RailsConf and you're attending,
or if you're planning on attending RubyConf, next year,
I would thoroughly suggest that you volunteer
to become a guide,
and help the program get bigger.
It's something that our community
is applauded by other communities, for having.
I've seen people from a very, very wide range
different language and technology communities,
say our Scholarship program is great.
So, I would encourage you all, to take part in that.
So, with all of that, sort of, front matter out of the way,
let's get in to talking about coding, and stuff.
So, I wanted to start this talk with a story.
And, the story, actually, comes
from one of the question and answer sessions
from another one of my talks.
My talk was about deep, internal architecture
of how RSpec Mocks, actually works.
How the pieces all fit together, and stuff like that.
And, as the close of the explanation of the talk,
I said that most of the code was there
to enable us to deal with complicated user objects
that override default methods provided on Kernel or object.
And as I was beginning to answer the questions
from the developers,
more senior and deep technical questions,
got out of the way, I was asked a question
by a junior developer that, absolutely, floored me.
That I wasn't, actually, able to give
a reasonable and complete answer to.
And this is one thing I absolutely love
about working with less experienced developers,
because they don't have all of this base-line,
built-up, implicit knowledge,
you're forced to explain things in a great level of detail
and clarify your own understanding of what those things are.
The question that I was asked is,
"When is it okay to override the methods on object?"
And I'd said that sometimes it's okay
and sometimes it's not,
but, I hadn't really given a clear explanation
for when that was the case.
So, I sort of, um'd and ah'd,
I gave half explanations.
I really wasn't able to fully clarify my thoughts.
And, this is so often the way,
when you get asked a basic question
that you think you totally understand.
You, actually, can't explicitly put the knowledge
into words and say it, and this is, again,
one of the things I love working with newer developers.
So, eventually, I came to an answer
to do with consistency.
When overriding methods on object
is sometimes a good idea, and sometimes it isn't.
And the answer to do with consistency
was well, do it when it makes your object
more consistent with the things that already exist in Ruby,
I could give lots of examples of this,
I could talk for hours and hours and hours
about this subject, but, that is not the focus of the talk.
So instead, I'm gonna give a quick example and move on.
So, for example, let's imagine
you have some collection object that you've implemented,
that might be a bit like a hash or a string or a set,
or something like that.
Well, if you don't override
the equals equals method on that object,
then it's going to just perform object identity comparison
But, that's not very Ruby like.
Object identity comparison is fine, in some cases,
but when you have a clearly defined collection of things,
what most of the Ruby standard library does
is ask all of the things inside it
if they match all of the other things
inside of the other collection.
And so, doing and override on this equals equals, there,
is totally reasonable and this is a way
that you make your object more consistent, not less.
So, I'm gonna go through the rest of this talk
and it would be great if you could hold on to that story
as we're walking through it
and we'll visit it back at the end of the talk.
So, I want to posit the question,
"What makes a gem good?"
Now, this is a hotly debated topic in our community,
and, I certainly can't provide you with a universal panacea,
but, I can provide you with I think makes a gem good.
And, you can decide to agree or disagree with me,
as you want to.
So, what about the internals?
Does it matter to us, if the internals of a gem
are crazy, in order for a gem to be good, or not?
Well, I don't really think so, like,
we've all worked with, a lot, of very complicated libraries,
every day, but it's very rare that we look inside them.
I suspect, most people in this room,
aren't intimately familiar with the source code
that makes up Ruby on Rails,
or Sinatra, or Rake, or RSpec.
So, clearly, the internals don't matter to us that much,
day-to-day, when we're picking
which gems we're going to use.
I suspect most of you don't read the source code
of all the gems that you use.
But what about the interface?
Well, to me, I think this is a winner.
I think, most of us, go after gems
that have nice interfaces,
that are going to work well with our applications
and to some degree, it's something to do with convenience.
I think a gem is good, if it's more convenient,
than I were to do the equivalent implementation,
within side my application, myself.
I mean, I would like to think
I'm a pretty capable Ruby developer,
and so, given any arbitrary problem,
I can probably get it done,
in some reasonable amount of time.
So, if I'm going to pull a prepackaged piece of code
off the shelf, then, it has to be more convenient
to fit within my application,
than if I was going to do it myself.
There has to be a time/benefit trade-off there.
It has to stay convenient.
As my application grows, my architectural patterns
are going to change the way that my team works
and functions, is going to be different,
and so on and so on.
A gem that you pull into your application, now,
may seem like a great idea,
but in six months, or twelve months,
you may, absolutely, hate yourself.
I'm sure, some of you have made that decision.
Teams are not homogeneous, in terms of skill.
We have senior developers, we have junior developers.
We have people who have been programming,
other programming languages than Ruby,
for longer than they've been programming Ruby.
So, if that's the case, we need our gem and our interface
to be understandable by everyone on our team.
Powerful enough to do what the senior developers want
and easy enough to understand for the juniors.
Now, this one might be, slightly, further out,
but, I suspect some of you have applications
that don't, just, run on MRI, some of you run on JRuby,
or maybe, Rubinius, or maybe, you'll have,
like, a hybrid Ruby deployment
in different parts of the cloud or, whatever.
So, if you're gonna pull a gem off the shelf,
it needs to work with a wide range of Ruby code bases,
and each works with different Ruby interpreters.
It needs to do, a lot, of different things.
So, I've just come up with a really long shopping list
of criteria for what I want the gem to be.
But, it wouldn't be fair to have that,
without comparing it with the gem
that I write and maintain, and see how it stands up
to everything that I've just asked for.
Now, I could stand here and tell you
that RSpec is definitely, always convenient,
but, that would be a lie.
I'm sure every single person in this room,
who has used RSpec, at some point,
has decided they absolutely hate it
and don't want to use it anymore.
I certainly know I have, on multiple occasions.
So, that's fun.
So, if we're going to talk about RSpec,
let's pick a specific feature, go through it,
and see how it stands up.
To do this, I'm actually going to do a User story.
Now, I'm sorry to get all agile on you,
just before lunch, but here we are,
and, hopefully, it will be fine.
As an RSpec user, when I stub an object,
I want the original method to be on that object
after the example, so that my objects
aren't broken by my test suite.
That was, a little, fast.
And, I'm sure not all of you, spend all day,
mocking back-and-forth, in RSpec.
So, let's go through that, again,
with a, slightly, more detailed explanation.
As an RSpec user, that's about 70% of you in the room,
according to the last year's Ruby survey.
When I stub an object, that's the process of doing, like,
allow object to receive foo,
or if you're still on legacy RSpec,
object.stub(:foo), or whatever.
What that's gonna do, is replace that method
and change it so that instead of invoking,
whatever the original implementation of that method is,
instead it calls into fake RSpec codes,
so that you don't have to execute,
whatever that objects implementation is.
I want the original method
to be back on that object after the example.
So, that is, when we're done stubbing an object inside RSpec
we put the original implementation back,
and so here, we're actually talking about the process
of moving methods around in Ruby,
I'll explain how that works in just a minute,
so that my objects aren't broken.
I would contend, that most of you don't have RSpec stubs
moving around in your production systems.
And therefore, like, it's not reasonable for RSpec
to leave a stub on an object
after your test is done executing,
because that leaves the object in an inconsistent state
with how it would be otherwise, in the system.
So, we need to do that research.
Okay, so we're talking about moving methods around,
taking them off objects and putting them back on.
How does that work?
Well, just in case, you aren't doing this everyday,
the syntax that we're talking about, here,
in RSpec, is the allow(cat). to receive (:meow) syntax.
Allow, to and receive, there, the RSpec key words,
and I'm making up some hypothetical object called (cat),
which has a hypothetical (meow) method.
And what this is going to do,
is it's actually going to remove the (meow) method
from the (cat), save it somewhere else,
execute your test and then put the original method
back on the (cat) object.
So, during the execution of the test
you'll have the faked (meow) method
and when the test is done
you'll have the original implementation back.
So again, we need to pull another layer back
and talk about how you actually save methods
and move them around in Ruby.
If you head over to the standard library documentation
and you look really, really close
at the documentation for objects
you'll find this method called "method".
The definition of method, according to the documentation
is method is a method which takes a symbol
and returns a method.
So far, so cryptic.
Let's read the doc string and see if that helps.
Looks up the named method
as a receiver in obj's, returning a method object
or raising name error.
The method object acts as a closure
in obj's object instance,
so instance variables and the value of self
Everyone clear on that?
(audience member comment too low to hear)
So, what we're talking about, here,
is the concept of a method object.
Ruby doesn't have first class functions
and, so, we can't simply treat methods on objects
as data values.
The method method allows us to do that.
When we invoke it with a symbol
it returns to us an object
which represents that method
on that particular object instance.
You can pass the method object around,
as if it were any other object in the Ruby system.
The method object has a public method
called call, on it, which allows you to invoke the method,
provided and you pass the arguments to call,
that you would pass to the method.
This, basically, allows you to pass the method around,
as an object in Ruby, which is great for RSpec,
cause it means we can put that method
somewhere else, execute our test
and then put the method back at the end of the test
by using the method object,
and, basically, just shoving it back on the object.
We've saved the method using the method object
and then we can return it later.
Now, I wish I could tell you
that that was the whole story.
I wish I could tell you, that it really was that simple.
But, unfortunately, that would be a lie
and this would be the shortest talk at the conference.
So, now I have to talk about Defensive coding
and this is where it's going to get, slightly, mad.
In RSpec, we like to design abstractions
over common tasks in Ruby where we need to fix bugs
and, basically, provide places to hang user facing behavior.
For the rest of this talk
we're going to be discussing a method,
This is, basically, our wrapper around the method method,
to catch a bunch of user error cases.
Basically, if you want to work in Ruby
to pull method objects off user objects
simply invoking the method method
is not good enough, in a lot of cases.
And so, what follows is an evolution
through our get history, roughly altered
and made linear for various branching reasons
and cutting a couple of scenes out.
So, like, this is the roughly true evolution
of how that method came to be,
but, it's not the absolute truth.
Please, dive the get history
if you're interested in finding out more,
or you want to tell me that I'm wrong.
So, the first implementation
you can find anywhere in the RSpec's source history,
it looks like this.
is defined to take an object and method name.
It simply invokes the method method
on that object with the method name.
Basically, what we've done
is we've just wrapped the method method
in an RSpec method.
And actually, in the place where the code
actually was, this was just
object of method method, but story.
The first problem we have, here, is that users lie.
How many of you have ever had to do
anything to do with HTTP in Ruby?
Right, that's a ridiculous-- no, don't put your hands up,
that's a ridiculous question.
If you look inside the source code
for, basically, any HTTP library, anywhere in Ruby,
you'll find a method that looks like this.
We're gonna define a method, called method,
which returns the string 'get' or 'post',
or, maybe it's a symbol,
or like, maybe it sets the method on an object.
Something like this.
But, what the user has done, here,
is they've blown away the original implementation
of the method method.
We no longer can, just, directly invoke that method
in order to get method objects back.
And, one of the truths about Ruby,
is that users can and will redefine, any method,
at any time, and for, basically, any reason.
And that's fine, they should be allowed to do that,
like, the word method means something
in the language of HTTP,
that is a domain specific word in HTTP,
that definitely should be allowed to be defined by clients.
As it turns out, it's actually not to hard
to deal with this case.
So, the second definition
you see anywhere in the get history looks like this.
We define a constant called Kernel method method,
which gets assigned to it, the value of the expression,
So, we've seen the method method, already,
let's talk about the Instance method method.
The instance method method, is like the method method
except that it returns instance method objects,
not method objects.
An instance method object is, basically, the implementation
of a method on a class, without any specific object instance
in order to receive the method.
You can, basically, think of it as,
like, the classes blueprint implementation of the method
with no specific object upon which to be invoked.
It's the method object without the target.
Instance method objects come from classes and modules
instead of specific object instances.
The difference, here, being that you can pass them around
and give them an instance to be invoked on, later.
So, what we're doing, here, with our Kernel method method,
is we're grabbing the Kernel implementation
of the method method, which is always correct.
It's always going to give us a method object instance, back.
Then, we simply, bind the implementation
of the Kernel method method, to the object that's passed
and then, we invoke it.
We're, basically, entirely bypassing the user's definition
of the method method, at this point.
We're saying, we don't care, like if you're in HTTP library,
or whatever the hell it is, you've defined it as,
we're just going to invoke our own implementation,
which, is always correct.
The point, here, is users
can and will redefine core methods, at any time.
And that's fine, because in a lot of cases,
you can just grab Kernel's implementation of the method
and use that instead.
Nobody screws with Kernel, don't screw with Kernel.
You can define stuff on your own objects, that's fine,
but please don't redefine stuff in Kernel.
Okay, so the next one is that Ruby interpreters lie.
We have this problem,
where there are, like, a bunch of Ruby interpreters,
out there, and RSpec has to support all of them.
Some objects don't have Kernel in their inheritance chain.
And like, the alpha nerds in the room
will have had their brains,
immediately, jump to basic objects.
Which is true, basic object doesn't have Kernel
in it's inheritance chain,
but there's an even weirder class of objects
in the Ruby standard library,
which don't have Kernel in their inheritance chain,
and they look like this.
They include a dupe of Kernel,
as the first thing they do.
Firstly, I'm sure that most of you have never seen anyone,
actually, dupe a module.
That's a really weird thing to do.
The basic reason for this,
is so you don't have the name Kernel
in your inheritance chain,
but you do have all of the behavior of Kernel.
So, looking at this script, it's gonna, actually, invoke
the RSpec::Support.method_handle_for definition,
which, currently looks like this.
Now, if I run this script, on an MRI lay test,
I get the exact correct result out.
I get exactly what I'm expecting,
but, if I switch to JRuby and run it,
and, actually, I should say now,
this is fixed in JRuby, this talk was done a while ago,
before the current release of JRuby,
I'm not trying to be mean, but...
So, I get this explosion, it says,
bind argument must be an instance of Kernel,
bind at, whatever, inside JRuby,
and, basically, what's going on here,
is we don't have Kernel in the inheritance chain,
but, we're trying to apply one of it's methods
to an object.
This is called rebinding module methods
and it's a thing that you'll, kind of,
supposed to be able to do,
but, sometimes, not really on different Ruby interpreters.
As I said, RSpec has to support,
basically, every Ruby interpreter, under the sun.
We support MRI versions 187,
plus all stable versions of JRuby, and even REE.
How many of you have still got REE apps in production?
There's always one, oh, two, whatever.
There's, like basically, no one,
but still enough that we have to support it.
One thing that's worth noting, here,
is that RSpec does not support Rubinius,
and that's not out of a lack of trying, on both sides.
We've had conversations with Rubinius' team,
they've had conversations with us,
it just turns out that, like,
as Charlie Nutter once put it,
the RSpec Mocks test suite, is like a stress test
for the meta programming of any Ruby VM.
As it turns out, Rubinius is, like, still trying to work out
some of those internals and it's not quite there, yet.
So, if you know lots about Rubinius
and you're interested in making RSpec work there,
I would be really interested
to have a conversation with you, please come find me.
RSpec, also, works on Ruby versions on Windows.
This is a Windows spaced CI, on the Is Your Cloud
and it totally works, which is nuts.
Basically, we really, really try to support,
basically, everything your going to do,
so, yeah, you have a lot of Ruby interpreter support.
So, module methods.
In order to support this,
the RSpec::Support.method_handle_for definition
grew to look like this.
The first thing that's important to note, here,
is that we're, actually, conditionally defining methods
based on the result of this Ruby features
support rebinding module methods method.
Which, actually, itself uses the same trick,
but, switching on Ruby interpreter features.
Basically, we know at Ruby interpreter boot time,
whether or not, we have the features we need
to be able to do this,
and so, this is the function that does that.
If we're running on MRI, we can do this
if the Ruby version is greater than or equal to two,
but, otherwise we have to create a new anonymous module,
define a method inside it, pull the instance method object
out of that module and then try and bind it
onto an object which doesn't include that module.
If that works, we define the method to return true,
but, if we get an exception, we define it to return false.
So anyway, coming back up here
to Ruby features support through binding module methods,
if we do, then we can simply use
the Kernel method rebinding trick,
but otherwise, we have to check if Kernel
is in the inheritance chain of the object
and if it is, we can use our trick,
otherwise, we have to directly invoke the method method.
Ideally, we wouldn't have to do that,
but, at this point, there's nothing else we can do.
The point, here, is that Ruby interpreters
behave differently and if you're are building a gem
which gets deployed to everyone,
it's a really good idea to make sure it works with them.
Just to show you this is real,
there's an object inside standard library
called simple delegator,
which, totally has all of the Kernel behavior,
but, does not have Kernel inside it's inheritance chain.
All right, moving swiftly on.
Sometimes, users don't lie.
Try not to, actually, read this code,
just, instead, follow along with the points.
So, here, I have an object, which has a single public method
and on another object, I define it's response to
and method methods to delegate onto that object,
if and only if, the method that's being invoked
on the outer object, is defined on the inner object.
Basically, here, I have correct implementations
of meta programming to delegate method calls,
down onto the other objects if they define those methods.
(audience asking question to low to here)
Anyway, moving on. (laughs)
Basically, what's gonna happen, here,
is because we've overridden the method method,
rebinding the Kernel implementation to it,
is going to give us an exception,
because the Kernel implementation,
doesn't know about this delegation logic.
Obdi is looking really confused.
Can I keep going, is that okay?
So, anyway, when this happens,
we have this weird catch 22 situation,
where we can't deploy our Kernel method trick,
because it doesn't know about the user's implementation
of the method method,
but, we can't directly invoke the method method
on most user objects, because they might override
the method method, and then we'd explode.
So, the solution, here, is a, like, trust but verify,
kind of, situation, and our eventual implementation
looks something like this.
Basically, we initially try the Kernel method method trick
and if that fails at raising a name error,
we invoke the method method on the user object,
check it returns a method handle
and if it does, we return it
and otherwise, we raise the original exception.
So, that's it.
That's everything you need to know
to pull a method object off a user object in Ruby.
It looks like this.
This is the complete implementation
again, don't try and read it, that's not the point, here.
So, to wrap up, this is why I love RSpec.
I think this is one of the things
that is to the credit of the RSpec framework,
beyond many of the others.
We will go to extreme lengths
to ensure, whatever, you do to your object
our framework does something reasonable,
when you pass it in.
That is an extremely hard guarantee to give,
but, it's also one that's, totally, worth it,
because, it enables RSpec to be used
in all kinds of legacy code situations,
it otherwise wouldn't.
Basically, if you're struggling with RSpec
and you're not sure, whether or not,
it's our fault, it probably is,
and, please, file a bug.
We'll be as nice as we can
and if it turns out not to be a problem inside of RSpec,
we'll fix up our documentation,
because it, clearly, means that you were confused
by something that we wrote.
Your gem should be defensive.
Users are going to redefine core methods
on object out from underneath you, at any time.
And when that happens, you should expect it.
Code will be run on interpreters
that aren't MRI, and interpreters that aren't MRI,
behave differently to MRI, you should expect that, too.
Ruby users are weird, look at us.
As a collective group, we're a strange, strange people,
and that's great, but, it also means
that you have to trust and verify, what they're doing.
So, at the beginning of this talk,
I asked a question, what makes a gem good?
So, what about the internals?
Well, I would argue that the explanation
that I just gave made, absolutely, no sense.
I would argue that, understanding the internals of RSpec
requires you to spend, literally, years of your life
learning about how Ruby meta programming works.
But, that's fine, because most of you
don't need to understand the RSpec internals.
What about the interface?
Well, you just type allow(cat) to receive(:meow)
and you get the engine coming to life.
You get everything inside the RSpec framework,
thousands and thousands of lines
of meta programming code activate,
in order to ensure that whatever you're trying to do,
it's just gonna work.
You probably didn't even know,
all of that nonsense was there
until I just showed you it.
And, that's fine, because we have a really good interface.
So, what about gems?
Well, to me, the point of a gem
is to provide a strong abstraction boundary
between what it is you're trying to do
and the internal implementation of how that works.
As Fanni Met says, the wrong extraction
can be really, really expensive.
The worst gems cause an extreme mess,
you have to monkey patch into them,
in order to get anything to work,
and that can be a giant pain in the ass.
Done correctly, you can hide huge amounts of complexity
behind really well defined barriers,
and part of that, is ensuring that when a user
passes an object into your gem, it doesn't explode,
when they've rewritten some core underlying piece of tech.
Write defensively, and your users
never gonna know what's inside the box,
they're not even going to look, it doesn't matter to them.
So, remember the story, at the beginning of this talk?
Where we had our confused junior developer?
Well, for me, this talk is really born
out of the power of Ruby and what you can do with it.
It's about making sure that our API is,
and our code is defensive in order to be prepared
against the mistakes of tomorrow.
I mean, just to sort of, quickly, draw a comparison,
this is the entire source code of MiniTest Mock,
as of yesterday.
Don't even bother reading it.
It's 169 lines of code.
I just showed you more code than that
inside RSpec, and that's not even a complete feature.
And, that's a trade-off,
there are definite trade-offs, there.
You can understand Minitest,
way more easily than you can understand RSpec,
but, there are differences in what you can and cannot do
when it comes to mocking objects in Minitest.
Ryan is nodding,
which means the thing I just said is correct.
So, I just showed you more code than that.
For a tiny complete feature that isn't a mocking library,
and that's fine, like, these are differences of opinion
and you should understand and consider them
before picking any of the trade-offs.
So, I'm done.
I just have to do one, slightly, gross,
please, check out the thing I'm working on, thing.
I'm making a sass called browserpath.
It's about load testing in front-end performance,
web applicationy stuff, so, please check that out,
if you're interested.
That's it, I'm done, peace out.
Let's have some questions.
Right, so the question is,
when Kernel is the inheritance chain,
can't you just grab the method off Kernel
and not care if it's in the inheritance chain?
The answer is, it depends,
on which Ruby interpreter you're using.
If Kernel is not present in the inheritance chain
of your object, on some Ruby interpreters
you cannot rebind methods taken directly off Kernel
on to those objects.
The other problem that you have,
is like, you really don't want to be searching backwards
in the inheritance chain of the object
because you're going to end up
finding, who knows what,
especially with how active records, and stuff, are built.
So, the solution that we have,
where we do our very best
and then, if it doesn't work we give up,
turns out to be fine on nearly every Ruby interpreter, now,
and, it's the case, that the number of people
who are still running on aging Ruby interpreters
has gone way down, so, it's not too much of a concern.
But, at the end of the day,
our solution is the most reliable way I have ever seen
to pull a method object off any object in Ruby,
so, it works, if you can find a way to break it,
please, let us know, because we'll try and fix it.
So, yeah, it's reliable until you can prove that it isn't.
All right, thanks very much.