RSpec
RSpec: It's Not Actually Magic
Summarized using AI

RSpec: It's Not Actually Magic

by Noel Rappin

The video titled "RSpec: It's Not Actually Magic" presented by Noel Rappin at RailsConf 2015 dives deep into the complexities and internals of RSpec, a popular testing tool in the Ruby on Rails ecosystem, which is often referred to as "magic" by its users and critics alike. Rappin aims to clarify how RSpec works under the hood in order to enable developers to use it more effectively.

Key points discussed:

- Complexity of RSpec: Rappin acknowledges RSpec's complexity and explains that most of it arises from its expressive domain-specific language (DSL) and its flexible architecture, which accommodates multiple configurations and Ruby versions.

- Internal Mechanics: The video explores how RSpec processes and executes tests. Key components, such as the describe, it, expect, and matchers are explained in detail, emphasizing their roles as internal objects and classes within the RSpec framework.

- Execution Flow: Rappin walks through the crucial processes of creating an example group, executing test blocks, and managing expectations. He illustrates how RSpec creates an anonymous subclass of ExampleGroup, executes hooks, and assesses pass/fail outcomes based on example results.

- Matchers and Expectations: The presentation sheds light on how matchers function—particularly RSpec's implicit invocation of predicate methods—and how expectations are created and evaluated.

- Customizability: Rappin provides an entertaining example of configuring RSpec with emoji as function symbols, showcasing its flexibility.

- Mock Objects: The video concludes with a discussion on RSpec's mock framework, explaining how test doubles and mocks are implemented to isolate test contexts and control method behaviors without executing actual methods.

Conclusions and Takeaways:

- Understanding RSpec's internals can lead to clearer and more expressive tests.

- The complexity of RSpec, while intimidating, is manageable with familiarity and practice.

- RSpec's extensive features and flexibility allow developers to tailor their testing experience, making it a powerful tool within the Rails ecosystem.

00:00:13.200 I'm here to talk about what is certainly the most beloved and non-controversial gem in the entire Rails ecosystem: RSpec. Yes, exactly! Everybody loves RSpec, right? After this, Ryan's going to come out and give you a talk just about how much he particularly loves RSpec.
00:00:20.400 In the meantime, I'm going to fulfill my role here and set up all the things that he's going to say in the next talk. So, as I said, everybody loves RSpec. However, I have to admit that RSpec offends me aesthetically with no discernible benefit—it's a cargo cult.
00:00:37.760 And by the way, unless you think this is a new argument, this is from March 2011. This debate has obviously been going on for a long time about RSpec's complexity and its utility. I'll just own it right here: RSpec is complicated. I'm not a member of the RSpec core team; I didn't write any of it. I use it all the time, but I have no ego wound up in saying that it's complicated.
00:01:05.199 Internally, RSpec is complicated. While I do think that it has discernible benefits, and I would be happy to go over that some other time, here we're mostly going to talk about the complicated part.
00:01:22.159 So, when we talk about RSpec being complicated, the things I'm going to discuss today are not so much the complexity of the domain-specific language that it presents to developers writing tests using it, but rather the internal complexity of RSpec. There are a couple of reasons why RSpec is complicated.
00:01:51.520 I suspect the RSpec core team would talk about how expressive RSpec is and how much trouble there is in the RSpec codebase that is specifically there to allow you to express your tests in a way that is closer to natural language than it is to straight-up Ruby.
00:02:06.000 A lot of the complexity of RSpec internals goes toward supporting that. RSpec is also very flexible; it supports two completely different syntaxes and includes the deprecated version two syntax. It runs on a lot of different Ruby versions and in various contexts.
00:02:32.879 And obviously, that's also true of all popular frameworks in this ecosystem, but that incurs a certain cost in terms of the internal complexity of the codebase. RSpec is large and has features that other test libraries don't, for better or worse. It has a very full-featured mock framework attached to it.
00:02:46.959 Additionally, it has a very full-featured matching framework. In JavaScript, you could easily be doing that with three separate libraries, but RSpec bundles them all together.
00:03:10.560 Just to offer a quick example of how flexible RSpec is: with about ten lines of configuration—all exposed by the RSpec API—I was able to create an executable RSpec with my own configuration mapping thumbs-up emoji to describe, eyeball emoji to expect, and heart emoji to equals. Three of those four I did using features that will be discussed shortly. RSpec's flexibility is evident even in such unconventional applications.
00:03:40.720 My name is Noel Rappin. I work at a consulting shop called Table XI in Chicago. I have stickers, and we are hiring. If you want to talk about either of those things, you can find me. That's why I wear the green hoodie. Hi everyone! It's so hard to see with this glare that I can't really tell whether people are out there. There are people out there, right? Okay, hi people!
00:04:32.720 So, we're going to be looking at some RSpec source code from the actual RSpec codebase, the current GitHub master as of a couple of days ago. This is your last chance to bail before we start looking at RSpec code up close. If you have sensitive stomachs, you know, fasten your seatbelts; make sure your tray tables are in their upright, unlocked position. Don't avert your eyes.
00:05:00.000 But I do want to say: why is it important, or why do I want to talk about the internals of RSpec? I don't expect that all of you will come out of this learning a little more about RSpec as users—though you will probably pick up a trick or two that you could use in your own tests. That's not really why I'm doing this.
00:05:36.000 RSpec is a really interesting example of a domain-specific language written in Ruby. It employs some techniques that you might use if you want to try to write your own DSL. But, you know, that's not really why I wanted to do this. I've used RSpec almost every day of my professional life for about the past eight years, and it occurred to me that I really didn't have a serious idea of what it was actually doing.
00:06:21.920 I thought that was worth rectifying, simply because I was curious and wanted to know. So, that's what I'm going for here. Hopefully, you'll come out of this with a little bit more understanding of RSpec's internals.
00:06:43.840 I always tell people that technical talks—even technical talks—should tell a story. There should be a beginning, middle, and end. My story, I guess, is once upon a time, there was a test.
00:07:02.560 This is a minimal RSpec test; it's about as small as I could make it while still enabling it to do something recognizably. We're going to walk through the stages that RSpec goes through to convert this first to an internal representation and then to execute that representation and turn it into a pass or fail.
00:07:51.840 Now, this kind of looks like idiomatic Ruby but kind of doesn't. One useful tip when you're exposed to a domain-specific language in Ruby is to start parenthesizing it and fully qualifying it. If we do that, we come up with a perspective that reveals some important things.
00:08:39.520 We see that describe is a method that takes one argument and a block, where that argument is a constant name. We see that it explicitly calls out the self receiver. The expect method is also a method of whatever self is at that point. The key words in RSpec—the DSL—include describe, it, expect, and eq, each of which corresponds to internal objects or classes within RSpec.
00:09:56.680 For instance, describe maps to an RSpec concept called an example group, expect maps to something RSpec calls an expectation target, and then the two at the end is something that RSpec calls a matcher. If you're familiar with RSpec as a user, you probably know the terms example and matcher, but the terms example group or expectation target are less known.
00:10:54.019 In terms of this spec, the describe method defines an example group, which sort of encompasses the entire describe block. It is the example object that gets invoked, covering the entire block passed in. Each individual call gets its own example, with the expect method and its argument serving as the expectation target, while the matcher is represented by self.eq.
00:11:52.720 Now, when RSpec executes that code and loads it, the first thing it hits is that describe call, which creates an example group. Describe is actually defined somewhat indirectly as a class method on ExampleGroup to create new instances.
00:12:59.520 One thing to note about the RSpec codebase is that most code in RSpec has at least one more level of indirection than you would expect. So, in some cases, I will show you something along the RSpec call stack that isn't exactly the method you think you're calling because it's actually defined in terms of something else.
00:13:36.320 When you call describe, RSpec creates an anonymous subclass of ExampleGroup and executes the block argument in the context of that new anonymous subclass. So here’s a piece of that code from part of ExampleGroup—this is the process as the example group gets created.
00:14:28.960 The first thing it does is use Ruby’s Class.new to create a brand new class. The parent of this top-level describe is ExampleGroup itself, and as you nest calls, they become subclasses of each successive nested example group.
00:15:28.480 After this subclass gets created, it calls out to a method called setup, which does some setup—specifically, it mixes in the matcher and mock package.
00:15:53.680 Then RSpec uses the Ruby core method Module.exec to execute the block argument, if it’s there. Hold on to that thought for a moment. The Module.exec method uses the receiver as the example; it is defined in the Ruby core documentation.
00:16:45.280 When I call Module.exec, that block gets executed in the context of the receiver as a class. What that means is, for the purposes of Module.exec, inside that block, self resolves to that outer object, treating the contents of the block as though they were effectively inside a class definition.
00:17:36.720 So this means when I say self.it, that 'self' inside the block refers to the example group subclass, and it is a method defined on that subclass, as are before and all those things being methods on ExampleGroup that get executed here.
00:18:46.640 After describing how RSpec executes the describe block, we can say that calling the it method creates an example object. RSpec defines multiple ways to create example objects, including generic methods, specify, and example, as aliases that take in a description and metadata.
00:19:57.720 What happens when you call it is that RSpec holds onto that example object and its block, adding it to an array that serves as a class attribute of that anonymous example group class. This is a part of the code executed as the example gets created from the RSpec codebase.
00:21:11.680 At that point, RSpec has loaded all that it needs; it hasn't executed the block inside the 'it,' but it's holding onto it. At runtime, RSpec creates an instance of that anonymous example group class, and then a new instance runs all of its examples.
00:22:14.640 This example group runs through its examples in three stages. The example group has a method that goes through all the examples and utilizes another method to run each individual example, eventually spinning off to the example itself to handle its own run.
00:23:05.600 Let's look at some of that code. It may seem a bit small, but this is the function called on the example group when it is executed, and there are several stages involved. The first stage simply sets up things, and if RSpec has decided it wants to quit out, it just bounces right at the top.
00:23:59.360 It tells the reporter or the formatter that an example group has started, because the formatter might perform actions in response to that hook, and then it looks for context hooks for any of its descendants. Mostly this is bookkeeping inside a begin block; it also runs the before method if it's available.
00:25:06.560 After running all the examples, the method returns a true or false. It assesses nested children and determines a true or false result for the entire group based on its pass or fail status and the results of its children.
00:26:10.560 So this method is crucial; without it, RSpec wouldn't be able to run validations on its state. You can infer that the executing example requires a valid context or state before proceeding with its run.
00:27:21.440 Now, the key part of the process—when the individual example gets control—checks to see that it’s not pending and runs its before hooks. If an exception is raised because the test failed, RSpec holds on to that exception for reporting purposes.
00:28:38.880 At the end, it runs cleanup if there is any, transferring whether the test has passed or failed to the formatter again.
00:29:31.680 This is part of the skeleton that determines whether specs pass or fail by utilizing expectations and matchers. When you invoke an expectation using 'expect', you are essentially telling RSpec to create an expectation target.
00:30:40.000 So, what we had there with 'to eq' was essentially RSpec creating and handling a matcher. The matcher checks to see if every value you’ve compared are identical, thereby determining if the expectation holds true. In a nutshell, this influences the final result you will get from your test.
00:31:50.400 A unique trait of RSpec matchers is their implicit invocation, where certain prefixed methods defer to corresponding predicate methods on the object. For example, if you use "b_value", it checks the "valid?" predicate to determine if that method exists and returns the matching result.
00:32:41.000 Lastly, regarding the emoji trick: I took advantage of the define example group methods to define a method that utilizes emoji for RSpec testing. It’s an entertaining example of how customizable and flexible RSpec is in its syntax.
00:33:31.659 Before concluding, I'd like to discuss RSpec's mock package, focusing on how mock objects serve as test doubles. For instance, we can expect a user to receive a method call and return a value; however, a mock requires that the underlying method is not actually invoked, and this function must track how frequently that method is called.
00:35:11.040 RSpec needs to use Ruby’s method resolution—looking first at the singleton class, then instance methods, and so forth—to control how the mock behaves, ensuring the defined method returns the specified behavior instead of the actual method.
00:36:36.800 A property unique to mock setups is the use of the proxy pattern. RSpec sets up these proxies in the singleton class of the original object, ensuring that the mocked methods are isolated to a specific instance.
00:37:43.200 In summary, handling expectations and matchers is a core aspect of RSpec, and it touches upon many elegant patterns reflected in Ruby's behavior. This presentation barely scratches the surface of how RSpec operates, but I hope you've gleaned some valuable insights.
00:38:34.960 If you want more information on RSpec, the documentation at rspec.info is quite decent. The codebase may appear daunting at first, but you'll find it’s not that hard to read once you become familiar with its indirection.
00:39:17.120 Feel free to reach out to RSpec core members if you have questions. Remember, I'm Noel Rappin from Table XI. I occasionally write books on related topics, and I encourage you to check out my works.
00:39:34.640 Thank you all for your time, and enjoy the rest of the conference!
Explore all talks recorded at RailsConf 2015
+117