Metaprogramming
Building a mocking library
Summarized using AI

Building a mocking library

by Andy Lindeman

In this video titled "Building a Mocking Library" presented by Andy Lindeman at Ancient City Ruby 2013, the speaker explores the construction of a simplified mocking library as a means to delve into advanced Ruby topics such as metaprogramming and the Ruby object model. The session emphasizes understanding the underlying mechanisms of mock object libraries, which often operate with a degree of abstraction that can obscure their workings from developers.

Key points discussed include:

- The motivation for creating a mocking library, as a tool used widely in the Ruby community but not commonly understood.

- A brief overview of the syntax that will be used in the library, including methods such as allow, expect, receive, and return, which facilitate creating mock objects and stubs for testing.

- The concept of a Domain Specific Language (DSL) that allows easy interaction with underlying methods and the importance of building a solid object base for the DSL.

- An exploration of Ruby's method dispatching and the significance of understanding inheritance and singleton classes in Ruby when building the library.

- The hands-on development of the library, which includes creating tests using MiniTest, defining methods within classes, and managing state within the mocking library to handle stubbing and unstubbing methods correctly.

Throughout the talk, several coding demonstrations illustrate how to construct the library incrementally, along with practical examples like stubbing methods and creating expectations on method invocations. The session also addresses potential pitfalls, such as how to restore existing methods after stubbing and ensuring that the library behaves correctly even in edge cases affecting method visibility.

Concluding remarks emphasize that constructing such a mocking library not only deepens understanding of Ruby but also equips developers with skills applicable to broader coding practices, especially in testing scenarios. Lindeman encourages attendees to experiment with the code and find ways to enhance functionality further, promising that features like expect and allow could potentially be integrated into future versions of RSpec.

Overall, the video serves as both a technical tutorial and an insightful exploration into Ruby's capabilities, inspiring developers to deepen their knowledge of the Ruby language and its testing frameworks.

00:00:00.240 Good morning, I'm Andy Lindeman. You can follow me online in various places at a Lindeman. I work for Big Nerd Ranch, and I really enjoy working there. Big Nerd Ranch writes high-quality books.
00:00:06.640 We also provide training and do consulting. I'm also a maintainer of RSpec, a Ruby testing framework you might have heard of. I'm also writing a book called Upgrading to Rails 4, so check that out if you're interested in Rails 4.
00:00:17.359 But this morning, we're going to be talking about building a mocking library or a mock object library. This is a hands-on coding talk, so make sure you're strapped into your seats, and I think you'll really enjoy it.
00:00:40.800 So, mocking libraries are a tool that many of us use in the Ruby community, but I don't think we always understand how they work. Methods magically appear when you write things like object.stub or object.stubs. Methods magically disappear at the end of tests. The stubs automatically get cleaned up when you set things like mock expectations, which some other speakers have talked about, like object.should.receive or object.expects, depending on your framework. These things magically get verified at the end of tests. But I think it's worth knowing how the tools you use work under the hood and how to build simple replicas of them. That's what we're going to do this morning; we're going to build a mock object library, and I hope you're surprised at how many tools the Ruby language gives you to build one of these pretty easily.
00:01:30.240 Okay, we're going to take a step back and talk a little bit about what the syntax of the library that we're going to build today looks like. I've already decided on this syntax, and it's nothing exactly like anything that exists currently, though as I'll speak to later, RSpec may support this syntax relatively soon.
00:01:49.200 So, imagine you have a warehouse test double, a warehouse being an object that knows how to store items and knows if items are included in this warehouse. Imagine we have a production warehouse code object, but in tests, we're going to fake it out and use a test double. I'll hit on test doubles yesterday. If we want to set a stub, we can just kind of replay what the warehouse should respond to. If we wanted to say, 'Hey warehouse, act like you're full and respond to the full? method with true,' we're going to write some syntax that looks like this: allow w to receive the full message and return true. So then, at any point in the test after that, we can call w.full and get back true.
00:02:13.120 If we want to set a mock expectation, again in the library we're going to build today, we're going to say something like expect w to receive the remove message with a given item ID. That says, 'Hey, we better receive w.remove(1, 2, 3, 4); it better be called at some point during the test.' That's a mock expectation. Are you with me? Awesome! Let's talk a little bit about what that syntax compiles to underneath the hood. We have this nice-looking DSL here, but we're going to have objects underneath the hood. When you build a DSL like this, it's a good idea to immediately delegate to some object layer underneath you; it makes it a lot easier to test and reason about.
00:02:58.560 Okay, so what does something like allow w return? We're going to call that a stub target in our system. It's going to wrap the w object and allow it to have stubs put on it. We're going to call this stub target, and you'll see when we get into code that all we do is call stub target.new. When you say allow, this fluent interface, this chained interface, is going to build up an object I'm going to call an expectation definition. It builds up information about the method that's about to be stubbed, like the name and the return value. So I could write code that looks like this and have the same effect.
00:03:23.920 And again, this is a good idea when building a DSL, a domain-specific language, that you can construct the thing underneath it relatively easily, and the DSL is just a nice layer on top of it. Okay, I'm going to have some audience participation in this slide. Let's talk about Ruby's method dispatch. If I have an object called o and I call 2s on it, how does Ruby know what code to run? How does it find 2s? Anyone? What was that? It starts looking in the current class. Yeah, it starts looking at the current class, but it could go up to superclasses, right? Ruby calls this the ancestor chain, and you can get at it by calling o.class.ancestors, and it returns you a list that's meant to be read from left to right.
00:04:14.159 I got that right, left to right? It's right to left for me! So it starts looking at object and says, 'Hey, object, do you have a 2s method?' And in fact, object does, so that's the code that gets dispatched. But what if I sent in a message like the magic id method, the __id__ method? That's not on object; it's on basic object. Well, philosophically, what would happen is Ruby would start at the left and say, 'Hey, object, do you have a magic id method?' Nope. Kernel? Nope. Basic object? Yep! And that's the code that would get invoked.
00:05:11.200 Except, I left a little detail out: the singleton class. It's not listed here, but objects in Ruby have singleton classes because Ruby allows you to define methods that are only on one object. If I write some code that looks like this: o.define_singleton_method, giving the symbol hello and returning a block hello bang string, I can call o.hello and get back hello. But no other object in the system has a hello method; only o. If I construct a new object, it does not have hello, and the method by which this happens is Ruby defines these singleton methods on the singleton class for specific objects, and each object has a different singleton class.
00:06:31.280 When we're going to build our mock object library, we'll be manipulating objects on singleton classes for objects. So that's all I want to talk about before we dive into some code. You all ready for this? Yes!
00:06:37.680 All right, here's my terminal. It's white; actually, not black. We're going to make our mock object library a gem, and it's going to be called Ancient Mock. The bundle gem command just creates some files that will be useful when building a gem. It's just some scaffolding, so let's cd into the directory and open up vim because everyone here uses vim, so everyone will be familiar with that, right?
00:07:04.480 I was really surprised yesterday at how many people were using vim and tmux, which is my setup. If you're not familiar with it, I'm not going to do anything too tricky, but that's my editor of choice. Okay, let's look at a file. Let's look at the gemspec to start off with. It's got some details that I would want to fill out if I were actually publishing this gem, but of course, I'm not, so I will just leave them blank. But it does have things like development dependencies down at the bottom, and I'm going to add a new development dependency for MiniTest.
00:08:00.800 Now, you might be asking yourself, 'Why is an RSpec maintainer using MiniTest?' and it's mostly because I'm using some syntax that currently conflicts with the way RSpec is built. Because RSpec may eventually support the syntax I showed, I would conflict with expect, so I'm using MiniTest. It's a perfectly great testing library, and that's just to make sure I don't run into some weird edge cases. Okay, let's look at a file in the lib directory that was created for us by the bundle gem command. It's just four or five lines of code here; one of them brings in a version constant.
00:08:37.440 I'm going to change this to require relative, so I don't have to deal with as many load path issues in the tests, and then it creates a place for us to put some code, and that's where we'll be putting code. Now, I told you I was going to use MiniTest and do this in a test-driven development fashion, so I hope you learn something about my workflow when writing tests, and we are going to write this test first.
00:09:16.080 So let's open up a file in the test directory. You can see this down here; I'm just opening up test_ancient_mock_test. I need to create the test directory and save the file. Let's write some scaffolding code that we just need to get some stuff running. Let's pull in MiniTest::Spec, MiniTest::AutoRun, so this file just runs as a regular Ruby file, and let's require_relative the file from the lib directory called ancient_mock. That brings in our gem's code. If we've hooked up everything correctly, we can write something like describe ancient mock do, and then this is where we're going to put our tests. This looks really similar to RSpec, but it's MiniTest::Spec.
00:09:58.760 I have a key sequence in my editor to run tests, and you'll be seeing this quite often. So let's see it work. Running the tests, and there's no tests, but at least we can see that we've hooked up everything correctly. Let's write the first test: it allows an object to receive a message returning a value. We'll say this is the simple stub case that we saw on the slides. Let's create a warehouse and just make it an object.new. Again, warehouse is just a test double we're going to use throughout because it's an easy thing that hopefully everyone understands.
00:10:41.200 Warehouses have items and you can manipulate them. Let's write allow warehouse to receive the full? message and return true. This is the syntax that we defined in our slides. If this test does what we want it to, we should be able to call warehouse.full and it must equal true. I was interested in the talk yesterday because I kind of just, without even thinking, kind of separate the given, when, and then portions of my test by whitespace, so I just kind of do this automatically. But I found it really interesting that it might be a good idea to formalize these things.
00:11:37.680 Basically, given a warehouse, allow it to receive a message, then test that it can actually receive that message. Okay, let's run the tests, and the first thing it blows up because there's no method called allow—undefined method allow for this test class. It looks kind of weird, but that's just the MiniTest::Spec dynamically created class. Alright, let's fix that. Let's switch back over to the production code file and define a module called TestExtensions.
00:12:03.600 TestExtensions is going to have these methods like allow, and allow takes in some object, and like we said on the slides, it's just going to immediately construct a stub target. What does stub target do? Well, for now, let's just define it to take in an object in its constructor and save it off as an instance variable. Okay, down here at the bottom, let's reopen the MiniTest::Unit::TestCase class and include AncientMock::TestExtensions. So this brings in the TestExtensions module, which has things like allow, and we'll eventually have methods like receive and expect.
00:12:59.200 Alright, let's run the tests. We're a little bit farther along now. Undefined method receive? So Ruby is trying to evaluate the right side of this expression. This part over here. Let's fix that by switching back over to the production code and defining a new method called receive that takes in a message name like full?. Like we said on the slides, this is going to start constructing an expectation definition, and we'll pass in the message name in the constructor.
00:14:05.840 Let's define expectation definition, have it take a message name in its constructor, save it off to an instance variable, and we'll actually expose this as an attr_reader so that things from the outside can say, 'Hey, expectation definition, what message were you defined with?' Let's run the test again. We're a little bit farther now, and return is undefined. And this is where some neat stuff starts happening: the tests are actually telling me exactly what code I need to write.
00:14:59.440 Basically, there's an and return method; it's undefined and it's undefined on expectation definition, so that's where I need to put it. And return takes in a return value, saves it off as an instance variable. We'll also expose the return value, oops, as an attr_reader. And importantly, we need to return self from and_return. This is pretty standard when you're doing a fluent interface in an object-oriented language: you keep building up an object and return self every time to support chaining on more methods.
00:15:53.919 Since we're writing code that looks like receive and return, and eventually we'll have receive with and return, you return self every time so the object just keeps getting built up. Okay, let's run the tests now. There's an undefined method to for stub target. That's the next method we need to write. This is, if you look at the test, what's happening is allow is returning a stub target, and we're trying to invoke the to method on it to make that stub happen.
00:16:50.759 Going back over to the production code, let's scroll up here to stub target and define to. Now, to is getting called with one argument, an expectation definition, and we'll just shorten it to be definition as a local variable here. Now what does it need to do? Well, it needs to define a method, right? And to do that, I'll use the syntax I showed in the slides: obj.define_singleton_method. The first argument is the method name I want to define. Again, I'm defining a method dynamically here on the singleton class.
00:17:37.759 So, definition.message—that's the message name—and what's the implementation? Well, just the return value. Just give back the return value. If we've hooked up everything correctly, we have our first passing test: we've stubbed a method on an object in about five minutes or so, and in about 42 lines of code. Except I hope some of you are a little uncomfortable. We've just defined a method on an object and left it there.
00:18:41.440 Most mock object libraries clean up after themselves, and ours is not. This is best demonstrated by yet another test, right? So let's write another test to demonstrate the problem. It removes stubbed methods after tests finish. Let's create another warehouse object. Let's allow it to receive the same message. This is the line from above. Let's make up a new method on our AncientMock module called AncientMock.reset that will reset the state back to what it was previously. Most frameworks have a method like this. RSpec calls it reset as well, and the implementation is going to be something like, 'Hey, go back to the state before you started stubbing things on things,' so go back to the point before we had a full? method.
00:19:55.360 So if we try to invoke full? on warehouse, what should happen? Exactly! So we'll use assert_raises to say, 'Hey, if we try to call warehouse.full? here, it should raise an exception.' Alright, let's run the tests. Undefined method reset for the module. So that tells us where to put the code. Let's switch over to the production code. We'll put it toward the bottom here and since it's a module method, I'll use def self.reset that puts it at the module level.
00:20:59.760 And I'm not sure what to do here because if you look back up at stub target, when we define a stub, we're just defining a singleton method and going on with life. We actually have no registry of methods that were stubbed. We don't even have a list of methods that we stub, so it's pretty much impossible to do a reset right now because we don't know which methods our library stubbed and which ones are just there beforehand.
00:21:53.840 We could probably do some crazy Ruby magic to figure this out, but that's not what I'm trying to demonstrate. What we need is some sort of separate class that defines stubs but also keeps track of what stubs exist. Because I am such a creative namer, I'm going to name a new class called Stubber that is initialized with an object that saves it off as an instance variable, and it has a method called stub that also takes in a definition.
00:22:44.160 We're going to move the code that's currently up here in stub target down into the Stubber class, and instead of stub target doing the definition ourselves, we're going to create a new Stubber and delegate down into the Stubber class to do the stub. Now, right now we've done functionally nothing different; the code works exactly the same, and in fact, we can verify this by running the tests again and seeing that the first one still passes. But the second one is still failing, of course, but it's failing for a slightly different reason now.
00:23:37.920 Now that we've defined the reset method, we expected that there was a NoMethodError to be raised, but nothing was raised, so we've got a little bit of work to do still, but at least we've gotten a refactoring that didn't break entire portions of the system. Okay, so Stubber.stub is going to do something a little different. It's actually going to save the list of things that were stubbed in an array called definitions, so it's going to shovel on each definition before it stubs it.
00:24:44.400 So it keeps state around about what methods were stubbed. Definitions are just going to be an empty array initialized in the constructor. Okay, this is closer, but we still don't have a list of all the methods that were stubbed because this Stubber instance immediately goes out of scope and gets garbage collected. So instead, we're going to create a custom constructor at the class level of Stubber that saves the Stubber instance in a list.
00:25:29.519 I'm going to name it for object, and it's going to take in an object, and it is going to construct a Stubber for that object, but it's going to save it off in a hash keyed by the object's ID, and it's going to lazily create it and save it for each object. One object, there's a one-to-one between an object and Stubber. This code looks a little weird, but it's actually pretty much straight from RSpec; RSpec does something really similar to this, RSpec calls them mock proxies, though, but it stores them in a hash keyed by object ID.
00:26:32.160 Well, we need to define stubbers. Stubbers is just at the class level going to be a lazily initialized hash. Now, instead of constructing a Stubber up here with .new, we construct it with for object, so that does the construction, returns an instance, and also saves it in a list. This doesn't get garbage collected because the Stubber class doesn't go away, as opposed to stock, right? Since we're defining stuff at the class level, this is kind of basic with effectively global state, but it's okay for now.
00:27:22.560 I'm not trying to do everything correctly, or else this talk would be five hours long. Okay, what do we want to do now? We want to have one method to call to just reset everything at once. Let's make up some code that we wish existed at the class level of Stubber called self.reset, so we can call reset on Stubber and just everything gets cleaned up. What code would we have to write to make that happen? Well, we would want to tell each stubber to go reset itself.
00:28:18.880 And we don't have a method to do that yet, but let's just assume that we're going to create one. I do this all the time, just like I will write a reset method later. So loop through each Stubber instance that was saved in that global hash and reset it, and then also clear out the list of stubbers because the list of Stubbers is no longer valid after they reset themselves. Okay, I know you're thinking, 'What does reset do?' Let's get there now.
00:29:13.920 At each instance of Stubber, we have a list of definitions, a list of stubs. What should reset do? Well, it should go through each of them and remove them. So let's loop through each definition, and unfortunately, there's not any public way that I know of to remove methods from a class. So we're going to have to get a little weird here, but that's okay because we're a mock object library; we're allowed to get dirty.
00:30:12.240 We're going to eval, we're going to evaluate a block of code in the context of obj's singleton class. This is effectively just writing code between class statements in Ruby. So any code that gets run here is kind of run in the context of obj.singleton_class, and we can write private methods because we don't have to give an explicit receiver anymore; because self in this block is obj.singleton_class, so we can just write remove_method definition.message if it was defined.
00:31:26.239 If method defined? definition.message. So remove the method if it was defined. Okay, it's been a while since we've run the test; it's making me a little nervous. So let's run them. And nothing is still happening yet. Well, we're still getting a failure because no exception was raised. We've built up all these reset methods, but we haven't called them in the one reset method that really matters, AncientMock.reset.
00:32:30.080 So let's go down to the end of the file and call Stubber.reset here. Stubber.reset, if you'll know, goes through each Stubber, resets it, and clears everything out. Running the test, we have two passing tests, so we have the stub and the unstub case. Alright, except there's a third case, right? What if the method existed already? What if there were already a full? method that was defined elsewhere? We're just removing it, right? So we're killing state that existed already on the objects.
00:33:21.440 Again, this is best recognized as a test. Let me write that; hopefully, it'll make sense. It preserves methods that originally existed. Let's create a warehouse object, as usual. Let's define a method on the warehouse singleton class. This is yet another way to define methods on Ruby singleton classes.
00:34:23.520 This defines the full? method to return false, so the warehouse is initially not full; full? returns false. Let's stub it as true, let's reset, and we expect it to go back to false. So it was originally full? before the stubber got involved, stubbed it as true; reset should go back to false. Except, you guys know what's coming right—no method error, because we just nuked it. We just took it away, no matter what existed there before. So let's fix that.
00:35:44.800 Let's go back over to the production code. Let's go down to the def stub method. The def stub method does not care what existed beforehand; it just defines a singleton method. If there was already a singleton method, it's just gone. So really what we want to do is before defining a singleton method, we want to preserve something. Preserve the method if it already exists.
00:36:54.880 How do we do that? We can ask the singleton class: does a method already exist on you with this name? If o.singleton_class.method_defined? definition.message. Hey, if you've already got a method defined on you, let's get a method object that represents that code. Ruby doesn't necessarily have first-class functions, but it does have method objects. You can actually ask Ruby for an object representation of your method, and you get at that by saying object.singleton_class.instance_method definition.message.
00:37:46.080 This returns a method object that looks very similar to a proc or a lambda, except it was created via the implementation of the method. Let's save that off in an array in an instance variable called preserved_methods where preserved_methods is initialized in the constructor to be an empty array. So if a method was already on the class that we're about to run over, we preserve it.
00:38:44.240 Now, down here in reset, after we've removed the methods, we really want to loop through the ones that were preserved and redefine them as their old implementation. So let's do that: preserved_methods.each do |method|. A small nuance here, we really want to restore them in reverse order. Ordinarily, I would write a test for this, and it's a great candidate for a test, but because I want to show you guys so many things, I'm going to skimp on test coverage a little bit here.
00:39:41.840 But otherwise if you stop thinking something multiple times, the wrong implementation would be restored if we didn't reverse do them in reverse order because we're building up the preserved methods list from left to right; we want to restore them from right to left. Okay, so that's what that means. Now when you have a method object, you can also use an alternative form of define_singleton_method to just redefine the method with a method object.
00:40:44.080 So, while up here we defined a singleton method with a block of code, down here we can just redefine the method by passing the method name and the method object itself into define_singleton_method. So Ruby's instance_method method that gives back a method object pairs really well with define_method and define_singleton_method because you can also take a method object to redefine the method with that method object that we originally saved.
00:41:41.760 Say that again? Yeah, not happening! Alright, let's run the test. We have three passing tests; we've restored the implementation of the methods properly, so we have stubs, unstubs, and then un-stub where there was an original method that we need to keep around. Alright, that was alright! So in 15 minutes, we've gotten a pretty sweet mocking library, or at least a stubbing library. We don't have mocks yet, though, right? Mocks are expectations where we expect things to get messages during the test.
00:42:50.880 So let's try that. It expects that a message will be received. Let's have a warehouse. Let me move this toward the top, and let's write, instead of allow, expect. Expect warehouse to receive, we'll say something like the empty method. Maybe the empty method says, 'Hey warehouse, clear out all your inventory.' So we're expecting that in the course of this test, warehouse.empty had better be called. But we're not going to call it. So when we call AncientMock.reset, what should happen? An error!
00:43:58.080 We're going to make up an error name: AncientMockExpectationNotSatisfied. So calling reset here because we did not call warehouse.empty; warehouse.empty was not called. Then an error will be raised by the reset method when it loops through each expectation and verifies that it actually got the message that we expected. Alright, let's run the test. The first thing that's wrong is there's no method called expect; we haven't added it to our TestExtensions.
00:44:49.280 So let's do that; flipping back to the production code, the TestExtensions is toward the bottom here. Let's add a new method called expect that takes in an object, but instead of a stub target, let's call it an expectation target because it's the target of a mock expectation. But it's really, really similar to stub target, so similar in fact that I feel pretty comfortable making expectation target a subclass of stub target because mock expectations in our library are still stubs.
00:45:59.520 The method implementation still gets stubbed out, but also we verify it, so we inherit stub target's constructor and also the two method. But we want to overwrite it to call into the super class's implementation of to, which does this but also to add the definition to a list of expectations that gets verified. So this is a specialized form of a stub: it's a stub that gets added to a list of expectations to later be verified.
00:46:51.440 Now, expectations doesn't exist, so let's create that. Go to the bottom of the file here and say def expectations. It's just a lazily initialized array, and it's a list we can use to verify those stubs that are also mocks that need to be verified. Now let's verify we haven't broken anything. We haven't, but our test did get a little farther; now it's an uninitialized constant ExpectationNotSatisfied. Well, that's easy enough to fix.
00:47:46.080 Let's go to the top of the file and define ExpectationNotSatisfied as a new class that inherits from StandardError. It's just an exception; it's a runtime error. Alright, run the test again. Isn't it great when you have fast tests? You can just keep running things; it tells you where to put things. Alright, now we actually get the failure we're looking for: 'ExpectationNotSatisfied' was expected to be raised, but nothing was raised.
00:48:34.160 So the block of code reset is not raising an error even when our mock was not satisfied, and it kind of makes sense, right? We haven't written any new code in self.reset down here; it's just resetting stubs; it's not verifying expectations. So let's fix that. I'm thinking that it makes sense to have some sort of verify method on each definition that verifies that it was called.
00:49:29.200 So let's just loop through each expectation and call something like verify on it. Now, we haven't written verify, but we will soon, and we're thinking, hey, verify will probably raise an exception if an expectation was not verified. So we need to make sure that, even if an exception is raised here, let's still reset all the stubbers because it'd be really annoying if your tests failed due to mock expectation errors and then also stubs just happened to stick around.
00:50:31.280 You would get some weird non-isolated tests, so let's ensure that everything gets reset. On top of that, we actually want to clear out the list of expectations as well so that we don't get weird feedback between tests. So even if one of the expectations fails, we still clear ourselves out and go back to normal. Let's run the tests, and we've broken it in a new and interesting way: undefined method verify for expectation definition!
00:51:45.600 We tried to call a method that didn't yet exist on expectation definition. Alright, that's easy enough to fix. Let's go to expectation definition and write a verify method. Can anyone think of a good implementation for verify? I can! Let's just raise an exception. Did anyone note that, like, I can make the test pass with that? So I know there's something else here I did, and we intuitively know there's something else, right? But since I've been running the test constantly, I can just write small bits of code and actually see that there's a gap in coverage here because all my tests passed, but that's clearly not right.
00:52:38.880 So let's fix that by writing a new and interesting test. But I'm going to leave the production code like that because it's clearly a gap in coverage. Okay, the new and interesting test is it does not raise an error if expectations are satisfied. Let's create the warehouse; let's expect that it receives a message empty, and then let's call it.
00:53:37.520 And then calling AncientMock.reset should not raise any errors, so we can just write it out in the test, and if it raises an exception, the test will fail. Running the test, they do, in fact, fail because an AncientMockExpectationNotSatisfied error was raised. Alright, let's go back to the implementation.
00:54:32.320 Really, what I'm intuitively thinking here is something like if invocation_count is not equal to one. Maybe most mocking object libraries allow you to customize the number of times you expect to receive a message, but we're just going to hard-code it as one for now; that'll be AncientMock 2.0. And where invocation_count starts out as zero in the constructor, but we don't have any way to increment invocation_count.
00:55:34.080 And in fact, if we look at the place where we define the method down here on line 52, all we do is just cough up the return value. We don't give definition an opportunity to increment that invocation_count. So I'm going to change definition to, instead of just coughing up the return value, I'm going to change it into a callable object. It's pretty standard practice in Ruby if you want an object to act like a callable piece of code like a proc or a lambda to just give it a call method. Definition doesn't yet have a call method, so let's add a call method.
00:56:36.560 Call is going to look something like this: still cough up the return value at the end, but also increment invocation_count. So this gives definition an opportunity to increase its own invocation_count while still returning its return value. Alright, let's run the tests, and we have five passing tests! So we've got stubs and mocks, basic ones anyway, but we don't have the ability to pass arguments to stubs.
00:57:25.440 We don't have any way to say that a method should receive arguments or receive certain arguments. So let's write a test for that: it allows objects to receive messages with arguments. Let's create a warehouse, and this is going to be a really interesting test. Actually, we're going to allow the warehouse back to stubs for now. Allow the warehouse to receive a message called include? which tests if the warehouse includes a given item by its ID.
00:58:12.480 So we're going to say we're going to stub out the warehouse to say the warehouse does include an item with ID 1234, but it does not include an item with ID 9876. So that one will return false. So include? 1234, true. Include? 9876, false. So if we call warehouse.include? with 1234, it should equal true, and if we include? on 9876, that should be false.
00:59:02.240 Running the test, the first problem is that we've made up this with method, but it doesn't exist, and it doesn't exist on expectation definition. Let's define it there. We're already in expectation definition here, so we can add a new method with that takes in a list of arguments, and we're going to use the splat operator to make the argument list an array.
01:00:05.040 Let's just save them off as an instance variable for now, and again, we got to return self to make the chaining work. Running the test again, the next error is an argument error: wrong number of arguments! One for zero! The quintessential very confusing Ruby error message that basically means the method was invoked with one argument, but Ruby only had its arity at zero; it only expected it to have zero arguments, and that happened on line 52.
01:01:06.640 So let's go to line 52. That's where we define our singleton method. Of course, when you use define_method with a block, the method is defined with the same number of arguments as the block, and the block takes zero arguments, so let's change that. Because now we have methods that take in arguments, let's have the block take in the splat arguments as well. Running the test again, we now get the failure, and failure is good! Failure is better than error.
01:01:51.840 You expected true but got false on line 61 of the tests. So let's switch back over to the test and look at this. This is where we try to call warehouse.include?(1234) and expect it to be true, except it's false. We can kind of intuit that what's happening is we're not doing any matching on the argument list. So just the last stub is winning no matter what the arguments are, and the last stub says return false.
01:02:57.360 So we could actually invoke include? with any arguments—it would always be false because that's just the last one, because when we call stub, you remember we just define the singleton method to invoke the last definition that we got. So this clearly needs to change; it needs to more like search for the right definition. How do we search for the right definition? Well, let's loop through the list of definitions.
01:04:23.680 We also have to do this in reverse order just to make sure that we get the last stub if they're stubbed for the exact same argument list. The last one should win. Again, this should be covered by tests, but I want to show you so many things that I don't have time for it. So let's loop through them in reverse order and detect that it matches! I don't exactly know how to match a definition yet, so I'm just going to make up a method that I will make exist later called matches? for each definition, and I'll pass in the definition name, the message name, and the list of arguments that were actually invoked for this specific definition call.
01:05:30.560 And once we find the right one, once we detect the right one—actually, detect is the same as find in Ruby. I'll use find because it's a little more clear. We can just call it. So, reverse through the definitions, find the right one, call it. This should work, right? Oh my goodness! What? We've broken all of the tests, or at least many of them!
01:06:30.720 Undefined method reverse for nil class! Let's look at this again. Definitions is clearly defined, right? Except it's not! In a singleton method definition like this, in a dynamic method definition, self is changed, and instance variables are looked up on self. So self here is actually obj; the object is self in here is not the same as self out here! It's pretty confusing, but it actually, I mean, it makes sense, right?
01:07:22.000 Like self should be the receiver of the calls. So this block is a closure; it can take on its environment, but not instance variables. One easy fix is to just use a local variable and define a local variable outside the block to be the instance variable: definitions. So now this block can access the local variable. Alright, running the tests, the next thing is that there's undefined method matches? because we haven't defined that for expectation definition. Expectation and definitions don't know how to match themselves yet.
01:08:14.560 So let's fix that. What does it mean to match as an expectation definition given a message and an argument list? Well, the message should match the message name it was defined with, and we will either match if we weren't defined with any arguments at all, if arguments is just nil. Or the list of arguments matches the list of arguments that were passed in.
01:09:10.720 This looks a lot like I'm defining some sort of equality operator here, but of course equal equal can't really take multiple arguments at least not and have it look cleanly, so and matches? also is a stronger name, so I like it better! Okay, let's run the test, and we have six passing tests! We have mocks and stubs with arguments in the right order, so I'm pretty psyched.
01:10:09.440 So our library is pretty rocking. There are some caveats, though, that I do want to address because I'm sure there's some advanced drivers out there that are like, 'What's going on?' If you call with, for example, and pass arguments that were not expected at all, you'll get something like a NoMethodError because the detect will return nil, and then you'll try to call call on nil.
01:11:07.360 Our spec, for example, just raises a separate error that says, 'Hey, you called this stub in a way that wasn't we don't know how to fix that.' So that wouldn't be hard to add; you could add some sort of, like, null callable thing that detect returns by default, and then that gets called and just invokes an error, but I'll leave that to you guys. Define_singleton_method and singleton_class are object methods.
01:11:56.160 So our object library would kind of blow up if you tried to stub something on BasicObject. This is not incredibly difficult to fix, but it makes the code a lot uglier because you have to use kind of more syntactic Ruby methods or syntax Ruby syntax to get at things like the singleton class, so it just makes things look a little uglier. So I didn't go down that road.
01:12:51.200 Method defined? and method_defined? returns false on private methods as it probably should, but that also means that if you try to stub a private method—why are you trying to stub a private method? But if you try to do this, people do; we would not restore it correctly, so I'll leave that up to astute readers to figure out.
01:13:31.680 And then, of course, we would want to integrate reset automatically probably at the end of the test. That's not that hard to do in both MiniTest and RSpec. In MiniTest, you hook into teardown; we define a module that defines teardown, and make sure to call super if you do that. And then in our spec, you would use an after hook.
01:14:34.840 Our library is Ruby 1.9.3 and above only, as far as I can tell. It's a really big pain in the butt to maintain a mock object library on something before that; there's just weird behavior and things, so I didn't even try to go down that road. But even given these things, I think our library is pretty cool, and it's pretty cool that we were able to build it in Ruby in about 30 minutes.
01:15:31.600 It's very likely that allow and expect will make their way into RSpec to be kind of a new DSL for doing RSpec mocks, so look out for that, follow the issue on GitHub if you're interested. I will push this to GitHub soon. Please don't use it, but if you do want to hack on it to understand mocking more, if you want to write some additional tests and try to figure out how to make AncientMock do the right thing for you, feel free.
01:16:22.640 And there's a link to the slides; I'll also tweet them shortly via my Twitter account: a Lindeman. Thank you so much; I hope you learned some things, and I had a great time.
Explore all talks recorded at Ancient City Ruby 2013
+1