00:00:07.970
Alright, this is Extremely Defensive Coding. Let's get started. Phil introduced me, but to introduce myself, my name is Sam Phippen.
00:00:10.110
You can find me on Twitter as Sam Phippen and on GitHub as sumfin. If you're interested in finding out more about me, you can check out my profiles there.
00:00:19.250
If you check out my GitHub profile, you'll find that I spend most of my time committing as a member of the RSpec core team. That's one of my driving motivations for coming to conferences like this. I think it's really important that RSpec is well represented in the community at large, and I love hearing questions and taking feedback.
00:00:36.450
So if you have anything you'd like to say about RSpec to someone who spends a lot of time working on it, you should take this opportunity to do so. If you see me walking around the conference, feel free to grab me, and let's have a chat. I love talking about this stuff. I work for a company called Fun and Plausible Solutions. We're a consultancy based in the UK, focusing on testing and refactoring Rails applications, doing object-oriented design and testing, and even some machine learning.
00:01:02.940
If that sounds interesting to you, please come and have a chat with me. I'm a little bit ill, so you might be able to hear it in my voice, and this talk might sound a bit spaced out because I've had a lot of medicine. But I'm sure we’ll be fine.
00:01:27.420
With all that sort of front matter sorted, let's actually talk about the topic. I like to start this talk with a story. The story actually comes from a Q&A session from one of my other talks about the internal mocking architecture of RSpec. As the questions came, and I answered the deeper technical details that the senior developers in the room were interested in, I was left with a few hands raised.
00:01:42.090
I received a question that absolutely floored me. It wasn't easy for me to answer, and this question came from one of the junior developers in the room. What I found interesting was how relatively easy it was for me to respond to the questions from the more experienced developers, but a more genuine question from someone newer in our community genuinely stopped me. This is one of the reasons I absolutely love working with junior developers: they force you to make explicit things that you have in your mind that you may not be able to fully articulate until they ask a question.
00:02:40.480
The question was, 'When is it okay to override methods that Ruby provides on Object?' The basic thesis of my talk is that RSpec is complicated because we have to do things to defend against changes users make to Object. I said that this is why it can be hard and that you really shouldn't do this most of the time—except, of course, 'most' is like a wiggle word and doesn't give you a useful definition of when to do it and when not to.
00:03:03.099
When I was asked this question, I couldn't immediately provide an answer. I sort of hesitated and walked around, giving half-explanations because I wasn't sure I was doing the explanation justice. So, I stopped, took a breath, and really thought about it. I tried to come up with an answer that unified all of the examples and half-explanations I'd given. This is a really useful exercise. Again, it's one of the reasons I enjoy working with junior developers: they force me to explain what I'm talking about.
00:03:59.410
The answer that I came up with had to do with consistency. The idea is that you should override methods on objects precisely when they make the object more consistent with Ruby, not less. To give an example of what I mean: there are things in the Ruby standard library that override methods provided by Kernel to make them behave more as you'd expect.
00:04:14.380
A good example of this is `==` on a data object like a string or a collection. Because in that case, comparing object identities—which is the default behavior on an Object—doesn't really make sense, comparing the values inside the collection is going to be a lot more beneficial for everyone involved. The basic idea here is that by overriding `==`, you've altered some behavior of the object, but you've done so in a way that makes the object behave more consistently with all the other objects in a Ruby system.
00:05:01.850
I'd like you to hold on to this story as I go through the rest of the talk, and we'll come back to visit it at the end. Now, I want to ask the question: what makes a gem good? Your mind might immediately spring toward the internals of a gem—the core pieces of functionality that it provides, the things that actually make it do what it's supposed to do.
00:05:18.190
But the more I think about this, the more I find that the internals of a gem don't really matter. It's the interface that the gem provides that really makes me care about what the gem is going to do. The reason for this is that I like to think I'm a pretty capable Ruby developer, and if that's the case, I can basically implement anything a gem is going to provide me, given enough time and resources. So, if that's true, a gem has to be more convenient than if I simply implemented the same thing myself.
00:06:19.450
If I do the implementation myself, the code is going to fit in nicely with the existing architecture of my application. It's going to work how I want it to, and I'm going to understand what the API is. In order for a gem to be more convenient than if I did it myself, it needs to meet several criteria. One of them is that it has to remain convenient as my application continues to grow and gets built upon.
00:07:03.180
If a gem fits in immediately when I use it to get something done, that's great. But if, in six months, I find I can't easily modify my application because I've glued all of this code onto a gem, that's going to be a significant problem. It's reasonable to assume that most of us work on teams made up of developers with diverse skill ranges. If that's the case, a gem can only be used by developers who fully understand what's going on; it won't be nearly as convenient as if a gem can be used by everyone on the team.
00:08:02.469
Gems also need to work with a wide range of Ruby code bases. Not everyone in Ruby is just building Rails applications, and not everyone in Ruby is just running on MRI. A gem therefore needs to fit with all of these different scenarios that developers might encounter. That's a sizable shopping list, and it's only fair that I apply that to something that I work on and use every day. Now let's talk about RSpec.
00:08:32.880
I could say that RSpec is always convenient, but that would be an absolute lie. I'm sure everyone in this room has at some point encountered something in RSpec that has made them feel frustrated, angry or ready to smash their computer and log off for the day. I know this has happened to me, and if that's the case, we need to examine some pieces of RSpec in detail to see how they fit into this story.
00:08:52.840
To illustrate this, I'd like to explore a user story from inside RSpec. As an RSpec user, when I stub an object, I want the original method on that object to remain intact after the example so that my objects aren't broken by my test suite. This might be a bit fast, and I'm sure not everyone here is an expert in the language of mocking and stubbing inside RSpec.
00:09:20.260
So, let’s go through that again a bit more slowly. When I stub an object, I'm replacing a method on that object with a simple RSpec stub implementation to make it easier to reason about what that method is doing in my test. I want the original method to be on that object after the example so that when the test finishes executing, RSpec puts the method back on the object, completing it as it was before the test began.
00:10:24.390
This is important because nowhere in your production system are you going to have RSpec stubs floating around objects. So, it doesn't make sense to have RSpec stubs present on your objects during the execution of subsequent tests. What we're dealing with here is RSpec effectively moving methods around the system to provide convenience when you do stubbing or mocking.
00:10:43.810
You might reasonably ask, how does that work? Just to clarify, we're talking about the syntax from RSpec: the `allow` method that enables some object to receive some method. For instance, `allow(cat).to receive(:meow)`. What this does inside RSpec is save the `meow` method of the `cat` object somewhere else in the RSpec system. Your test will then execute, and once it's complete, the method is restored.
00:11:22.840
When the test execution is complete, the `meow` method will have its original implementation given back to the `cat` object. However, this raises the question: how do you save a method in Ruby? I suspect not everyone here spends their time moving methods around the system as if they were data, which is a relatively odd Ruby use case.
00:11:53.050
If you check the standard library documentation for Object, you'll find that there's a method called `method` which takes a symbol and returns a method object. The literal docstring reads, 'Looks up the named method as a receiver in self, returning the method object or raising a NameError.' The method object acts as a closure for an object's instance, meaning instance variables and the value of self remain available.
00:12:43.440
Now, that's not totally clear, but we're speaking here of something called a method object. In Ruby, every object that inherits from Kernel has a method called `method`, and when you invoke it with a symbol, it returns a method object. Method objects represent a single method on a single object somewhere in the Ruby system, and you can move them around as though they were any other object. This allows you to treat methods as data.
00:13:47.720
Method objects have a single public method called `call`. When you execute the `call` method, it's like invoking that method directly on the object. This capability allows us to effectively implement our feature in RSpec since we can take a method object, place it somewhere else, and restore it at the end of a test. However, I could tell you that that's everything you need to implement method saving in RSpec, but unfortunately, that's not entirely true. To convey the truth of the matter, we need to talk about something I'm going to label defensive coding.
00:14:50.220
We're going to specifically discuss a method from within RSpec called `rspec/support/method_handle`. This is essentially our equivalent of the `method` method, except that it actually works. In many cases, invoking the `method` method isn't sufficient. To explain why this is the case, let's quickly go through the history of how this method has evolved.
00:15:23.830
Some scenes have been altered and sequences adjusted, but this will provide an approximate view of its true representation given that I have slides and not the ability to travel in time. The first implementation of `rspec/support/method_handle`, which you'll find anywhere inside the RSpec history, takes an object and a method name and directly invokes the method method on that object.
00:15:43.460
This function calls the `method` method and, as I mentioned, is unfortunately not good enough. One reason it's insufficient is that users can redefine core Ruby methods. How many of you have worked with HTTP in Ruby? I know that's a bit of a stretch, but consider the issue. If you look inside any HTTP library in Ruby, you'll find there’s usually a definition for a method that returns something like `get`, `post`, or `put`, as a string or a symbol. This is fine; the term 'method' is significant within HTTP, and users should have the ability to define it.
00:16:36.020
Unfortunately, users can and will redefine methods at any time, for any reason. When this happens, the `method` method no longer reliably returns method objects as expected. Therefore, to solve this issue, RSpec has evolved to create a constant known as `Kernel.method_method`, into which we assign the result of the expression `Kernel.instance_method(:method)`. This allows us to obtain the implementation of a method without concern of redefinition.
00:17:30.960
The `instance_method` method essentially allows you to acquire the implementation of any method from any class without needing to focus on a particular target instance. Thus, what we've done is save the kernel implementation of the `method` method, and with that saved reference, we can call our `method_handle` method, binding the kernel implementation of `method` to the object using the method name to retrieve the results.
00:18:16.580
The key takeaway here is that users can and will redefine crucial methods in Ruby. This is something that the language explicitly allows. Therefore, we rely on the kernel's implementation of method retrieval because generally, no one tampers with Kernel.
00:18:36.560
However, Ruby interpreters can also present challenges. Some objects don’t inherit from Kernel within the inheritance hierarchy. While Ruby's object system is straightforward, there are objects in the standard library that inherit from BasicObject and include a duplicate of Kernel. That gives them access to Kernel's methods without explicitly incorporating Kernel into their inheritance chain. If we invoke our current implementation of `method_handle` and switch to JRuby, we might encounter an exception: 'bind argument must be an instance of Kernel.'
00:19:16.160
The process of rebinding module methods should work in all Ruby interpreters from version 2 onward, but in some cases, it simply doesn't function as intended. RSpec has to support essentially every Ruby interpreter available—this includes supporting Ruby 1.8.7 and up, along with all versions of JRuby and even Rubinius. The challenge is that the Ruby interpreter's behavior can differ significantly, complicating gem compatibility.
00:20:01.280
To ensure compatibility, we set up conditional method definitions based on Ruby interpreter characteristics during boot time. This is a performance optimization, as we could place the conditions inside the method, but that would be unnecessary. This feature tells us whether we can rebinding module methods, specifically if we're running on MRI Ruby version 2 or higher. If this process fails, we then capture the instance method object and attempt to bind it to objects that don't include that module.
00:20:35.310
This method also checks if the Kernel is present in the inheritance chain of the object. If so, we apply the rebinding method. If not, we resort to calling the `method` method regardless of the Ruby interpreter's behavior, knowing that it might yield different results based on the context and its particular quirks.
00:21:21.220
To ensure that RSpec continues offering support across various environments, we have developed a robust framework to tackle these issues. Importantly, we also support Windows environments through a cloud-based CI that resembles Travis—while it may be somewhat janky, it operates effectively.
00:21:49.060
Our goal is to embrace every Ruby interpreter and ensure that our test framework remains conducive to users. It requires us to develop intricate solutions for a myriad of situations, even when it may seem excessive. Our approach may include implementing a system that feels forced, as we navigate the complexities of various environments. RSpec comprises all the approaches we've explored, ensuring compatibility, but the catch is understanding how to support various methods and code bases.
00:22:18.890
One of the reasons I love RSpec is that we actively work to ensure that users with atypical redefining of the `method` method are supported. Users defining the method method to return method handles and working with objects that have broken inheritance chains are all accommodated. Our commitment to supporting Ruby code with our testing framework drives our dedication. If you can articulate your needs using the RSpec DSL, we aim to make it work for you. This is a very high bar for us to meet, but it's one that we aspire to achieve.
00:23:14.970
So please file bugs if you think RSpec isn't working for you. It may be our fault or may indicate some obscure bug deep within our engines, or potentially point to a documentation issue that we need to address. When writing a gem, you must be defensive: users can lie, redefining methods can emerge from various sources, and the `method` method may shift unexpectedly, causing challenges.
00:23:57.080
People may stub methods or respond to others in ways you might not anticipate. Your code must accommodate not only today's development conditions but also run seamlessly across all Ruby interpreters, especially within the various environments your application might operate in. We're supporting users who take unexpected approaches with their software.
00:24:31.480
Throughout this talk, I posed the question of what makes a gem good. What about the internals? While I argued that RSpec's inner workings might seem complicated, what's truly significant is that the user experience and the interface matter far more. You type `allow(cat).to receive(:meow)` and automatically get all that complexity behind the scenes activating with near perfection. And yet, that's not the most intricate part of RSpec.
00:25:04.720
The truth is, you get to leverage the workings of RSpec without needing to be aware of the intricacies below the surface, hidden behind a well-defined interface. In conclusion, the interface of a gem is what truly matters in ensuring it serves its purpose effectively.
00:25:45.510
The analogy stands: if an object is a barrier through which you can obscure code to facilitate abstraction, then a gem is a super-object—a much larger barrier where you can hide extensive blocks of code to form significant functionalities. Mistakes with abstraction can result in significant drawbacks, but when executed correctly, you can mask considerable complexities behind well-defined interfaces.
00:26:19.960
Defending against the unpredictable nature of software, particularly the changes your users could introduce, will enhance the convenience of the gem. Ultimately, your aim should be to prevent users from feeling compelled to look beyond the surface of your gem, preventing them from exploring the internals of `method_handle` implementations.
00:27:23.130
Do you remember the story I shared at the beginning of this talk? The question posed by a beginner is rooted in Ruby's power. Given that users can and will go ahead to redefine core methods for various reasons, we need to carefully consider the implications of that in the software we write.
00:27:39.620
We need to adapt our code today to defend against the mistakes we're likely to make tomorrow. That’s all I've got. Thank you very much, and let's have some questions.