00:00:00.240
good morning i'm andy lindemann you can follow me online in various places at a lindeman i work
00:00:06.640
for big nerd ranch and i really enjoy working there big nerd ranch writes high quality books
00:00:11.840
we also provide training and do consulting i'm also a maintainer of rspec a ruby
00:00:17.359
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
00:00:23.840
interested in rails 4. but this morning we're going to be talking about building a mocking library or building a mock object
00:00:30.000
library this is a hands-on coding talk so make sure you're strapped into your seats
00:00:35.280
and i think you'll really enjoy it
00:00:40.800
so mocking mocking libraries are a tool that many of us use in the ruby community but i don't think
00:00:45.920
we always understand how they work methods magically appear when you write things like object.stub or object.stubs
00:00:54.480
methods magically disappear at the end of test the stubs automatically get cleaned up when you set things like mock
00:01:00.239
expectations which some other speakers have talked about like object.should receive or object.expects
00:01:06.080
depending on your framework these things magically get verified at the end of tests but i think it's worth knowing how the
00:01:12.400
tools you use work under the hood and how to build simple replicas of them and that's what we're going to do this morning we're
00:01:18.240
going to build a mock object library and i hope you're surprised at how many tools the ruby language
00:01:24.640
gives you to build one of these pretty easily okay we're going to take a step back and talk a little bit about
00:01:30.240
what the syntax of the library that we're going to build today looks like i already decided
00:01:35.840
this syntax and it's nothing exactly like anything that exists currently though as
00:01:42.079
i'll speak to later our spec 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
00:01:55.439
how to store items how to know if items are included in this warehouse
00:02:00.799
imagine we have a production warehouse code object but in tests we're going to fake it out and use a test double
00:02:07.040
nell hit on test doubles yesterday if you want to set a stub just a just a
00:02:13.120
a kind of replay of what warehouse should respond to if we wanted to say hey warehouse act
00:02:19.040
like you're full respond to the full question mark method with true we're going to write some
00:02:25.760
syntax that looks like this allow w to receive the full message
00:02:30.800
and return true so then at any point in the test after that we can call w dot full and get back true
00:02:36.640
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
00:02:42.560
w to receive the remove message with a given item id and that says
00:02:48.560
hey we better receive w dot remove one two three four it better be
00:02:53.760
called at some point during the test that's a mock expectation are you with me
00:02:59.120
awesome let's talk a little bit about what that syntax
00:03:06.159
compiles to kind of underneath the hood we have this nice looking dsl here but
00:03:11.360
we're going to just we're going to have objects underneath the hood when you build a dsl like this it's a good idea to just
00:03:16.879
immediately delegate to some object layer underneath you it makes it a lot easier to test and it makes it a lot easier to reason about
00:03:23.920
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
00:03:31.040
object and allow it to have stubs put on it so we're going to call this stub target
00:03:36.720
and you'll see it when we get into code that all we do is just call stub target dot new when you say allow this part over here
00:03:44.159
to the right this fluent interface this chained interface is going to build up an object i'm going to call an expectation
00:03:50.480
definition it builds up a information about the method that's about to be stubbed
00:03:57.200
things like the name and the return value so i could write code that looks like this and have it have the same
00:04:02.879
effect and again this is a good idea when building a dsl a domain specific language
00:04:08.640
that really you can construct the thing underneath it relatively easily and the dsl is just a
00:04:14.159
nice layer on top of it okay i'm going to have some audience
00:04:21.519
participation in this in this slide let's talk about ruby's method dispatch if i have an object called o and i call
00:04:28.320
2s on it how does ruby know what code to run
00:04:33.520
how does it find 2s anybody what was that it starts looking in the
00:04:39.919
current class yeah it starts looking at the current class but it could go up to super classes
00:04:46.400
right ruby calls this the ancestor chain and you can get at it by calling
00:04:52.280
o.class.ancestors and it returns you a list that's meant to be read from left to right
00:04:57.600
i got that right left to right it's right to left for me
00:05:05.120
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
00:05:10.479
that gets dispatched but what if i sent in a message like the magic id method the underscore underscore id underscore underscore
00:05:16.560
method that's not on object it's on basic object well philosophically what would happen is ruby would start at the left
00:05:22.800
say hey object do you have a magic id method nope kernel nope basic object yep and that's the
00:05:29.280
code that would get invoked except i left a little detail out
00:05:35.360
the singleton class it's not listed here but objects in ruby have singleton
00:05:40.720
classes because ruby allows you to define methods that are only on one object
00:05:47.280
if i write some code that looks like this o dot define singleton method giving the symbol hello and returning a
00:05:53.360
block hello bang string i can call o dot hello and get back
00:05:59.759
hello but no other object in the system has a hello method only o if i construct a new object it
00:06:07.199
does not have hello and the method by which this happens is ruby defines
00:06:12.639
these singleton methods on the singleton class for specific objects and each object has a different singleton class
00:06:19.520
when we're going to build our mock object library we'll be we'll be manipulating objects on
00:06:24.800
singleton classes for objects so that's all i want to talk about before
00:06:31.280
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
00:06:45.840
we're going to make our mock object library a gem and it's going to be called ancient mock
00:06:52.960
the bundle gem command just creates some files that will be useful when building a gem
00:06:58.319
it's just some scaffolding so let's cd into the directory
00:07:03.840
and open up vim because everyone here uses vim so everyone will be familiar with that right
00:07:09.759
i was really surprised actually 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
00:07:15.440
anything too tricky but that's my editor of choice okay let's look at a file let's look at
00:07:20.560
the gem spec to start off with it's got some details that i would want to fill out if
00:07:26.319
i were actually publishing this gem but of course i'm not so i will just leave them blank
00:07:31.520
but it does have things like development dependencies down at the bottom and i'm going to add a new development dependency for mini test
00:07:38.000
now you might be asking yourself why is an rspec maintainer using mini test and it's mostly because i'm using some
00:07:43.440
syntax that currently conflicts with the way our spec is built because rspec may eventually support the syntax i showed
00:07:49.680
i would conflict with expect so i'm using mini test it's a perfectly great testing library
00:07:54.800
and that's just to make sure i don't run into some some weird edge cases okay let's look at a file in the lib
00:08:01.280
directory that was created for us by the bundle gem command it's just four lines or five lines of code here one of them
00:08:08.240
brings in a version constant i'm going to change this to require relative so i don't have to deal with as
00:08:13.280
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
00:08:18.800
putting code now i told you i was going to use minitest and do this in a test driven development fashion so
00:08:25.440
i hope you learned something about my workflow when writing tests and we are going to write this test first
00:08:31.120
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
00:08:39.120
i need to create the test directory and save the file let's write some
00:08:45.040
scaffolding code that we just need to get some stuff running let's pull in mini test spec mini test
00:08:50.640
auto run so this file just runs as a regular ruby file and let's require relative the file from the lib directory
00:08:58.080
called ancient mock that brings in our gems code if we've hooked up everything correctly
00:09:03.839
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
00:09:10.240
to our spec this but it's mini test spec i have a key sequence in my editor to
00:09:15.279
run tests and you'll be seeing this quite often so let's see it work running the tests and there's no tests
00:09:21.519
but at least we can see that we've hooked up everything correctly let's write the first test it allows an
00:09:29.360
object to receive a message returning a value we'll say
00:09:34.720
this is the simple stub case that we saw on the slides let's create a warehouse and just make
00:09:39.839
it an object.new again warehouse is just a test double we're going to use throughout because it's an easy
00:09:45.120
thing that hopefully everyone understands warehouses have items and you can manipulate them let's write
00:09:51.200
allow warehouse to receive the full question mark message and return true this is the syntax that
00:09:59.440
we defined in our slides if this test does what we want it to we
00:10:05.040
should be able to call warehouse.full and it must equal true
00:10:10.240
i was interested in in the talk yesterday because i kind of just without even thinking
00:10:16.240
kind of separate the given when and then portions of my test by white space so i just kind of
00:10:22.399
do this automatically but i found it really interesting that it might be a good idea to formalize these things but basically given a
00:10:28.880
warehouse allow it to receive a message then test that it can actually receive that message
00:10:34.560
okay let's run the tests and the first thing it blows up because there's no method called allow undefined method allow
00:10:42.240
for this test class it looks kind of weird but that's just the mini test spec dynamically created class all right let's fix that let's switch
00:10:49.760
back over to the production code file and define a module
00:10:54.959
called test extensions test extensions is going to have these methods like allow
00:11:00.320
and allow takes in some object and like we said on the slides it's just going to immediately construct a stub target
00:11:07.040
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
00:11:13.839
instance variable okay down here at the bottom let's
00:11:19.680
reopen the mini test unit test case class and include ancient mock test extensions
00:11:27.279
so this brings in the test extensions module which has things like allow and we'll eventually have methods like receive and expect
00:11:34.880
all right let's run the tests we're a little bit farther along now undefined method receive so it's
00:11:40.880
ruby's trying to evaluate the right side of this expression this part over here
00:11:46.800
let's fix that by switching back over to the production code and defining a new method called
00:11:54.160
receive that takes in a message name like full question mark like we said on the slides this is going
00:11:59.760
to start constructing an expectation definition and we'll pass in the message
00:12:05.920
name in the constructor let's define expectation definition
00:12:12.560
have it taken a message name in its constructor save it off to an instance variable and we'll actually expose this
00:12:18.399
as an adder reader so that things from the outside can say hey expectation definition what message were you defined with let's
00:12:25.839
run the test again we're a little bit farther now and return is undefined
00:12:30.880
and this is that this is where some neat stuff starts happening the tests are actually telling me exactly what code i need to write basically
00:12:36.959
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
00:12:44.720
and return takes in a return value saves it off as an instance variable
00:12:52.240
we'll also expose the return value oops as an outer reader
00:13:00.320
and importantly we need to return self from and return this is pretty standard when you're doing a fluent interface in an
00:13:06.480
object-oriented language you keep building up an object and return self every time to support chaining on more methods so
00:13:13.519
because we're writing code that looks like receive and return and eventually we'll have receive with and return
00:13:20.720
you return self every time so the object just keeps getting built up okay let's run the tests now there's an
00:13:28.800
undefined method to for stub target that's the next method we need to write
00:13:34.160
this is if you look at the test what's happening is allow is returning a stub target and we're
00:13:39.680
trying to invoke the to method on it to make that stub happen going back over to the production code
00:13:46.320
let's scroll up here to stub target and define to now to is getting called
00:13:53.680
with one argument an expectation definition and we'll just shorten it to be definition as a local variable here
00:14:00.480
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
00:14:07.760
showed in the slides obj.define singleton method it the first argument is the method name
00:14:14.720
i want to define again i'm defining a method dynamically here on the singleton class so definition.message that's the message
00:14:21.120
name and what's the implementation well just the return value just give back the return value
00:14:29.279
if we've hooked up everything correctly we have our first passing test we've stubbed a method on an object
00:14:36.480
in about you know five minutes or so and in about 42 lines of code
00:14:41.760
except i hope some of you are a little uncomfortable we've just defined a method on an object and left it there
00:14:47.120
most mock object libraries clean up after themselves and ours is not this is best demonstrated by
00:14:53.040
yet another test right so let's write it another test to demonstrate the problem it removes stubbed methods after tests
00:15:00.320
finish let's create another warehouse object
00:15:06.160
let's allow it to receive the same message this is the line from above let's
00:15:14.880
make up a new method on our ancient mock module called ancient mock.reset
00:15:20.000
that will reset the state back to what it was previously most frameworks
00:15:26.079
have a like a method like this our spec calls it reset as well and the implementation is going to be
00:15:31.759
something like hey go back to the state before you started stubbing things on things so go back to the point before
00:15:37.920
we had a full method so if we try to invoke full on warehouse what should happen
00:15:45.600
exactly so we'll use assert raises to say hey if we try to call warehouse.full here it
00:15:52.560
should raise an exception all right let's run the tests undefined
00:15:57.680
method reset for the module so that tells us where to put the code let's switch over to the
00:16:03.120
production code we'll put it toward the bottom here and since it's a module method i'll use defself.reset that puts it at the module
00:16:09.839
level and i'm not sure what to do here and i'm not sure what to do here
00:16:15.279
because if you look back up at stub target when we define a stub
00:16:22.000
we're just defining a singleton method and going on with life we actually have no registry of methods that were stubbed we
00:16:29.519
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 are
00:16:35.279
library stubbed and which ones are just there beforehand we could probably do some crazy ruby magic to figure this out but it's not
00:16:41.759
what i'm trying to demonstrate what we need is some sort of separate class that defines stubs but also keeps
00:16:47.759
track of what stubs exist so because i am such a creative
00:16:53.120
namer i'm going to name a new class called stubber that is initialized with an object
00:16:59.360
that saves it off as an instance variable and it has a method called stub that also takes in
00:17:05.120
a definition and we're going to move the code that's currently up here in stub target
00:17:10.640
down into the stubbor class and instead of in stub target doing the
00:17:16.880
definition of ourselves we're going to create a new stubber and delegate down into the stubborn
00:17:23.280
class to do the stub now right now we've done functionally nothing different the code works exactly
00:17:28.799
the same and in fact we can verify this by running the tests again and seeing that the first one still passes
00:17:35.360
but the second one is still failing of course but it's failing for a slightly different reason now because we defined
00:17:41.520
the reset method we expected that there was a no method error to be raised but nothing was raised so we've got a little bit of
00:17:48.480
work to do still but at least we've gotten a refactoring that didn't break entire portions of the
00:17:54.160
system okay so stubborn stubborn.stub is going to do
00:17:59.840
something a little different it's actually going to save the the list of things that were stubbed
00:18:05.520
in an array called definitions so it's going to shovel on each definition before it stubs it so it
00:18:12.640
keeps state around about what methods were stubbed definitions is just going to be an empty array initialized
00:18:17.840
in the constructor okay this is closer but we still don't have a list of all the methods that were
00:18:23.919
stubbed because this stubborn instance immediately goes out of scope and gets garbage collected
00:18:30.080
well so instead we're going to create a custom constructor at the class level of
00:18:36.000
stubborn that saves the stubbor instance in a list i'm going to name it for object and it's
00:18:42.559
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
00:18:49.679
keyed by the object's id and it's going to lazily create it and save it for
00:18:54.960
for each object one object there's a one to one between an object and stubber this code looks a little
00:19:01.919
weird but it's actually pretty much straight from our spec our spec does something really similar to this our spec calls them mock proxies
00:19:08.400
though but it stores them in a hash keyed by object id well we need to
00:19:13.520
define stubbers stubbers is just at the class level going to be a lazily initialized hash
00:19:19.919
now instead of constructing a stubber up here with dot new we construct it with four object so that
00:19:25.840
does the construction returns an instance and also saves it in a list
00:19:30.880
this doesn't get garbage collected because the stubborn class doesn't go away as opposed to stock
00:19:36.080
right since we're defining stuff at the class level um this is kind of basic with
00:19:41.120
effectively global state but it's okay for now
00:19:46.960
i'm not trying to do everything correctly or else this talk would be five hours long
00:19:52.400
okay what do we want to do now we want to have one method to call to just reset everything
00:19:57.440
at once let's make up some code that we wish existed at the class level of stubber
00:20:03.440
called self.reset so we can call reset on stubber and just everything gets cleaned up what
00:20:09.679
code would we have to write to make that happen well we'd want to tell each stubber to go reset itself
00:20:15.679
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
00:20:21.440
write a reset method later so loop through each stubbor instance that was saved
00:20:26.720
in that global hash and reset it and then also clear out the list of stubbers
00:20:31.760
because the list of stubborns is no longer valid after they reset themselves okay i know you're
00:20:37.200
thinking what does reset do let's get there now at each instance of stubber we have a list of definitions a
00:20:43.039
list of stubs what should reset do well it should go through each of them and remove them
00:20:49.679
so let's loop through each definition and unfortunately there's not
00:20:56.880
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
00:21:02.720
a mock object library we're allowed to get dirty we're going to clap we're going to evaluate a block of code
00:21:09.280
in the context of of obj's singleton class this is effectively just writing code
00:21:14.559
between class statements in ruby so any code that that gets run here is kind of run in the
00:21:21.520
context of obj.singleton class and we can write private methods because we don't have to
00:21:27.679
give an explicit receiver anymore because self in this block is obj.singleton class so we can just write remove method
00:21:34.200
definition.message if it was defined if method defined definition.message so remove the method
00:21:41.840
if it was defined okay it's been a while since we've run
00:21:47.360
the test it's making me a little nervous so let's run them and nothing is still happening yet
00:21:54.159
well we're still getting a failure because no exception was raised we've built up all these reset methods
00:22:01.120
but we haven't called them in the one reset method that really matters ancient mock.reset so let's go down to
00:22:06.880
the end of the file and call stubbor.reset here stubbor.reset if you'll know goes
00:22:13.360
through each stubber resets it and clears everything out
00:22:19.120
running the test we have two passing tests so we have the stub and the unstub case
00:22:26.480
all right except there's a third case right what if the method existed already what
00:22:32.400
if there were already a full question mark method that was defined elsewhere we're just removing it right we're just
00:22:38.320
we're so we're killing state that existed already on the objects again this is best recognized as a test
00:22:43.840
let me write that hopefully it'll make sense it preserves methods that originally existed
00:22:50.960
let's create a warehouse object as usual let's define a method on the warehouse singleton class this is yet another way
00:22:57.520
to define methods on ruby singleton classes this defines the full method to return false so the warehouse is initially
00:23:03.760
false is initially not full full returns falls let's stub it as true
00:23:13.039
let's reset and we expect it to go back to false
00:23:20.799
so it was originally full before the stubborn got involved stubbed it as true reset should go back
00:23:27.440
to false except you know you guys know what's coming right a no method error because we just nuked it just
00:23:34.799
we just took it away no matter what existed there before so let's fix that let's go back over to
00:23:40.960
the production code let's go down to the def stub method
00:23:49.200
the def stub method does not care what existed beforehand it just defines a singleton method and
00:23:54.799
if there was already a singleton method it's just gone so really what we want to do is before
00:24:01.279
defining a singleton method we want to preserve something preserve the method if it already exists
00:24:07.600
how do we do that we can ask the singleton class does a method already exist on you with this name
00:24:13.200
if oej.singletonclass dot method defined question mark
00:24:18.320
for the name of the method definition.message hey if you've already got a method defined on you let's get
00:24:25.520
a method object that represents that code ruby doesn't necessarily have first
00:24:31.679
class functions but it does have method objects you can actually ask ruby for an object representation
00:24:37.360
of your method and you get at that by saying object.singletonclass.instancemethod
00:24:43.880
definition.message this returns a method object that looks very similar to a proc or a lambda
00:24:49.679
except it was created via the implementation of the method and let's save that off in an array in
00:24:56.000
an instance variable called preserved methods where preserved methods is initialized
00:25:01.760
in the constructor to be an empty array so if a method was already on the class that we're
00:25:10.240
about to run over we preserve it now down here in reset after we've
00:25:17.440
removed the methods we really want to loop through the ones that were preserved and redefine them
00:25:23.760
as their old implementation so let's do that preserved preservedmethods.each do
00:25:29.279
method a small nuance here we really want to we really want to restore them in
00:25:36.640
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
00:25:42.559
many things i'm going to skimp on test coverage a little bit here but otherwise if you stop thinking something multiple times
00:25:48.320
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
00:25:55.279
from left to right we want to restore them from right to left okay so that's that's what that means
00:26:01.679
now when you have a method object you can also use an alternative form of
00:26:08.559
define singleton method to just redefine the method with a
00:26:13.919
method object so ins while up here we defined a singleton method with a block
00:26:19.760
of code down here we can just redefine the method by passing the method name
00:26:25.520
and the method object itself into define singleton method so ruby's instance method method that
00:26:31.760
gives back a method object pairs really well with define method and define singleton method who can also take a method object to
00:26:38.640
redefine the the method with that method object that we originally saved
00:26:44.960
say that again yeah not happening
00:26:50.000
all right let's run the test we have three passing tests we've restored the implementation of the the methods uh properly
00:26:57.919
so we have stub unstub and then unstub where there was original method that we need to
00:27:03.360
keep around all right that was all right so in 15 minutes we've gotten a pretty sweet mocking library
00:27:09.279
or at least a stubbing library we don't have mocs yet though right mocks are expectations where we expect things to
00:27:15.840
get a messages during the test so let's try that it expects that a message will be
00:27:23.919
received let's have a warehouse
00:27:29.120
let me move this toward the top and let's write instead of allow expect expect warehouse to receive
00:27:36.880
we'll say something like the empty method maybe the empty method says hey warehouse clear out all your
00:27:42.240
inventory so we're expecting that in the course of this test warehouse.empty had better be
00:27:48.080
called but we're not going to call it so when we call ancient mock.reset
00:27:53.120
what should happen an error we're going to make up an error name ancient mock expectation not satisfied
00:28:02.960
so calling reset here because we did not call warehouse.empty warehouse.empty was
00:28:08.080
not called then an error will be raised by the reset method when it loops through each expectation and verifies that it
00:28:14.640
actually got the message that we expected all right let's run the test the first
00:28:20.480
thing that's wrong is there's no method called expect we haven't added it to our test extensions
00:28:26.320
so let's do that flipping back to the production code the test extensions is toward the bottom
00:28:31.840
here let's add a new method called expect that takes in an object but instead of a stub target let's call
00:28:37.840
it an expectation target because it's the target of a mock expectation but it's really
00:28:43.679
really similar to stub target so similar in fact that i feel pretty comfortable
00:28:49.039
making expectation target a subclass of stub target because mock expectations in our library
00:28:56.080
are still stubs the method implementation still gets stubbed out but also we verify it
00:29:02.480
so we inherit stub target's constructor and also the the two method but we want to overwrite
00:29:08.960
it to call into the super class's implementation of to
00:29:14.240
which does this but also to add the definition to a list of expectations that gets
00:29:20.480
verified so this is a specialized form of a stub it's a stub that gets added to a list of
00:29:27.200
expectations to later be verified now expectations doesn't exist so let's create that
00:29:32.960
go to the bottom of the file here and say def expectations is just a lazily initialized array
00:29:41.120
and it's a list we can use to verify those stubs that are also mocks that need to be verified
00:29:46.880
now let's verify we haven't broken anything we haven't but our test did get a little farther
00:29:52.320
now it's an uninitialized constant expectation not satisfied well that's easy enough to fix let's go
00:29:57.360
to the top of the file and define expectation not satisfied
00:30:03.760
is a new class that inherits from standard error it's just an exception it's a runtime error
00:30:11.440
all right run the test again isn't it great when you have fast tests you can just keep running things it
00:30:16.559
tells you where to put things all right now we actually get the failure we're looking for
00:30:21.600
expectation not satisfied was expected to be raised but nothing was raised so the block of code reset
00:30:28.000
is not raising an error even when our mock was not satisfied and it kind of makes sense right we
00:30:33.120
haven't written any new code in self.reset down here it's just resetting stubs it's not verifying expectations
00:30:39.840
so let's fix that i'm thinking that it makes sense to have some sort of verify method on each definition that verifies
00:30:46.720
that it was called so let's just loop through each expectation and call something like verify on it
00:30:53.520
now we haven't written verify but we will soon and we're thinking hey verify will probably raise an exception
00:30:59.760
if an expectation was not verified so we need to make sure that even if an exception is raised here let's still
00:31:04.960
reset all the stubbers because it'd be really annoying if your tests failed due to mock expectation errors and then also stubs
00:31:11.760
just happened to stick around you would get some weird non-isolated tests so let's ensure that everything gets
00:31:17.760
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
00:31:24.000
between tests so even if one of the expectations fails we still clear ourselves out and go back to
00:31:29.519
normal let's run the tests and we've broken it in a new and interesting way
00:31:35.679
no method verify for expectation definition we tried to call a method that didn't yet exist on expectation definition
00:31:43.360
all right that's easy enough to fix let's go to expectation definition and write a verify method can anyone
00:31:51.039
think of a good implementation for verify i can let's just raise an exception
00:32:00.799
did anyone note that like i can make the test pass with that so i know there's something else here i did like and we intuitively know there's
00:32:07.279
something else right but since i've been running the test constantly i can just write small bits of code and actually see that
00:32:13.279
there's a gap in coverage here because all my tests passed but that's clearly not right
00:32:18.320
so let's fix that by writing a new and interest more interesting test but i'm going to leave the production code like that because
00:32:24.960
it's clearly a gap in coverage okay the new and interesting test is it does not raise an error
00:32:31.760
if expectations are satisfied let's create the warehouse let's expect
00:32:38.559
that it receives a message empty and then let's call it and then calling ancientmoc.reset should
00:32:45.600
not raise any errors so we can just write it out in the test and if it raises an exception the test will fail
00:32:52.399
running the test they do in fact fail with because an ancient mock expectation
00:32:58.880
not satisfied error was raised all right let's go back to the implementation
00:33:04.559
really what i'm intuitively thinking here is something like if invocation count is not equal to one
00:33:11.360
maybe most mocking object libraries allow you to customize the number of times you expect to receive a message but we're
00:33:16.960
just going to hard code it as one for now that'll be ancient mach 2.0
00:33:22.320
but we and and then where invocation count starts out as zero in the constructor
00:33:30.559
but we don't have any way to increment invocation count and in fact if we look at the place where we define the method
00:33:36.640
down here on line 52 all we do is just cough up the return
00:33:42.480
value we don't give definition an opportunity to increment that invocation count
00:33:48.159
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
00:33:54.159
object it's pretty standard practice in ruby if you want an object to act like a callable piece
00:34:00.240
of code like a proc or a lambda to just give it a call method definition doesn't yet have a call
00:34:06.080
method so let's add a call method
00:34:12.639
call is going to look something like still cough up the return value at the end
00:34:17.839
but also increment invocation count so this gives definition an opportunity
00:34:23.280
to increase its own invocation count while still returning its return value
00:34:29.599
all right let's run the tests and we have five passing tests so we've got stubs and mocks basic ones
00:34:35.440
anyway but we don't have the ability to pass arguments to stubs we don't have any way
00:34:40.720
to say that a a method should receive arguments or receive certain arguments so let's write
00:34:47.599
a test for that it allows objects allows objects to
00:34:53.520
receive messages with arguments let's create a warehouse
00:34:59.680
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
00:35:06.320
warehouse to receive a message called include question mark which tests if the warehouse includes a
00:35:11.520
given item by its id so we're going to say we're going to stub out the warehouse to say
00:35:17.839
the warehouse does include an item with id1234
00:35:23.200
but it does not include an item with id 9876 so that one will return
00:35:30.560
false so include one two three four true include
00:35:36.880
nine eight seven six false so if we call warehouse.include with one
00:35:42.560
two three four it should equal true if we include
00:35:49.680
on 9876 that should be false
00:35:57.119
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
00:36:03.599
definition let's define it there we're already in expectation definition here so we can add a new method
00:36:11.040
with that takes in a list of arguments and we're going to use the splat operator to make the argument list an
00:36:16.400
array let's just save them off as an instance variable for now and again we got to return
00:36:21.599
self to make the chaining work running the test again the next error is an argument error
00:36:28.160
wrong number of arguments one for zero the quintessential very confusing ruby error message that basically means
00:36:34.800
the the method was invoked with one argument but ruby only had its arity at
00:36:39.920
zero it only expected it to have zero arguments and that happened on line 52 so let's go to line 52.
00:36:49.119
that's where we define our singleton method of course when you use define method with a block the method is
00:36:54.480
defined with the same number of arguments as the block and the block takes a take zero
00:36:59.599
arguments so let's change that because now we have methods that take in arguments let's have the block take in
00:37:05.760
the splat arguments as well running the test again we now get the
00:37:11.760
failure and failure is good failure is better than error you expected true but got false on line
00:37:17.680
61 of the tests so let's switch back over to the test and look at this this is where we try to
00:37:23.280
call warehouse.include1234 and expect it to be true except it's false we can kind of intuit
00:37:29.599
that what's happening is we're not doing any matching on the argument list right so just the last stub is winning
00:37:34.960
no matter what the arguments are and the last stub says return false so we could actually invoke include with
00:37:40.960
any arguments it would always be false because that's just the last one because when we call stub you remember
00:37:47.040
we just define the singleton method to invoke the last definition that we got so this clearly needs to
00:37:52.560
change it needs to be more of like search for the right definition
00:37:58.880
how do we search for the right definition well let's loop through the list of definitions
00:38:04.400
we also have to do this in reverse order just to make sure that we get the last stub
00:38:09.440
if they're stubbed for the exact same argument list the last one should win again this should be covered by tests
00:38:14.640
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
00:38:20.800
the one that matches i don't exactly know how to match a definition yet so i'm just going to make up a method
00:38:26.560
that i will make exist later called matches question mark for each definition and i'll pass in the definition name the
00:38:34.000
message name and the list of arguments that were actually invoked for this specific definition call
00:38:44.720
and once we find the right one once we detect the right one actually detect is the same as find in ruby i'll
00:38:49.839
use fine because it's a little more clear we can just call it so reverse
00:38:55.359
through the definitions find the right one call it
00:39:02.000
this should work right oh my goodness what we've broken all of
00:39:07.599
the tests or at least many of them undefined method reverse for nil class
00:39:14.400
let's look at this again definitions is clearly defined right except it's not um in
00:39:22.400
a singleton method definition like this in a dynamic method definition self is changed and instance variables
00:39:28.960
are looked up on self so self here is actually obj the object
00:39:34.160
it's self in here is not the same as self out here it's pretty confusing but it actually i
00:39:39.200
mean it makes sense right like self should be the receiver of the calls
00:39:44.640
so this block is a closure it can take on its environment but not instance variables
00:39:50.160
one easy fix is to just use a local variable and define a local variable outside the block to be the instance variable
00:39:56.320
cc definitions so now this block can access the local variable
00:40:02.720
all right running the tests the next thing is that there's undefined method matches question mark
00:40:08.640
because we haven't defined that for expectation definition expectation and definitions don't know how to match themselves yet
00:40:14.400
so let's fix that
00:40:20.240
what does it mean to match as an expectation definition given a message and an argument list well the message should match
00:40:28.480
the message name it was defined with and we should either we will either match if
00:40:33.680
we weren't defined with any arguments at all if arguments is just nil or the list of
00:40:39.359
arguments matches the list of arguments that were passed in
00:40:44.880
this looks a lot like i'm defining some sort of equality operator here but of course equal equal can't really take
00:40:50.960
multiple arguments at least not and have it look cleanly so and matches also is a stronger name so i
00:40:56.720
like it better okay let's run the test and we have six passing tests we have
00:41:02.400
mocks and stubs with arguments in the right order so i'm pretty psyched
00:41:15.839
so our library is pretty rocking um there are some caveats though that i do want to address because i'm
00:41:21.280
sure there's some advanced drivers out there that are like what's what's going on um if you call with for example
00:41:27.200
and uh pass arguments that were not expected at all you'll get something like a no method
00:41:32.640
error because the detect will return nil and then you'll try to call call on nil
00:41:38.319
our spec for example just raises a separate error that says hey you call this stub in a way that wasn't we don't know how to to fix that so that
00:41:45.839
wouldn't be hard to add you could add some sort of like null callable thing that detect return
00:41:50.960
by default and then that gets called and just invokes an error but i'll leave that to you guys define
00:41:57.359
singleton method and singleton class are object methods so our object library would kind of blow up if you tried to stub something on
00:42:03.280
basic object this is not incredibly difficult to fix but it makes the code a lot uglier because you have to use kind of more
00:42:09.359
syntactic ruby meth or syntax ruby syntax to get at things like the
00:42:14.720
singleton class so it just makes things look a little uglier so i didn't go down that road
00:42:20.560
method define the method defined question mark method returns false on private methods as it probably should
00:42:25.599
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 people people do
00:42:32.319
this we would we would not restore it correctly so i'll leave that up to astute readers to figure out
00:42:38.640
and then of course we would want to integrate reset automatically probably at the end of test that's not that hard to do in both mini
00:42:44.640
test and rspec in many tests you hook into tear down we define a module that defined tear
00:42:50.480
down make sure to call super if you do that and then in our spec you would use an
00:42:55.599
after hook our library is ruby 193 and above only as far as i can tell
00:43:00.960
it's a really big pain in the butt to uh maintain a mock object library on something before that there's just weird
00:43:07.680
behavior and things so i didn't even try to go down that road but even given these things i think our
00:43:14.000
library is pretty cool and it's pretty cool that we were able to build it in ruby in about 30 minutes
00:43:19.359
it's very likely that allow and expect will make their way into our spec to be kind of a new dsl for doing our spec
00:43:25.440
mocks and so look out for that follow the issue on github if you're interested i will push
00:43:31.839
this to github soon please don't use it it's but if you do want to hack on it to understand mocking
00:43:37.760
more if you want to write some additional tests and try to figure out how to make ancient mock do the right thing for you feel free and
00:43:44.720
there's a link to the slides i'll also tweet them shortly via my twitter account a lindemann thank
00:43:50.640
you so much i hope you learned some things and i had a great time