ruby double (rr)
time: 2010-08-14 14:18 ~ 18:25
name: ruby double (rr)
body:
ruby double (rr)
Before start, I recommend this article written by Martin Fowler for reference.
Mocks Aren't Stubs
To save time, here's the summary. In this realm, there are four kinds of double:
1. dummy - object only used to fulfil parameter requirements.
2. fake - object that do the job differently, but not intend to be used in real world.
3. stub - object that always return canned result or only work in a very subset.
4. mock - basically a stub but it also monitors program execution flow.
For dummies, we give them yellow books there's no much to talk about.
For fakes, usually we won't want to write them since they're too
heavyweight, time consuming to write, and very depend on application.
To name an example, we can consider Rails' built-in memory cache as a
fake of memcache cache. This might not be a good example since Rails
implements it differently, it freezes objects that it stores, totally
different behaviour... not a good fake I'll say.
On the other hand, stubs and mocks could be generalized to be implemented
in a library. That's where rr comes into play. In addition, rr provides another
attribute called proxy. Proxy could be applied to either mocks or stubs,
it is used to say that the underlying object with a specific method should be
called with a real object, instead of a canned result. We'll see examples later.
rr utilizes a technique known as "double injection", because mocks/stubs are
injected into objects, work like wrappers of real objects. If the underlying
method invocation is matched to mocked/stubbed method, the mocked/stubbed
method would be called, instead of the real method. This way, they provide
canned results for the real objects. (unless proxies come into play.)
Say we have a user object that want to be tested in a controller, how do we
pass that user to the controller, which uses User.find to find users?
user = User.new :name => 'Are Are'
We can do this in rr:
mock(User).find(1234){ user }
Then next time when the controller called User.find(1234), it would not go
to database to see if there existed a user id with 1234, but returned the user
we just gave to the mock. If we mistyped 1234 to 5678, say this:
mock(User).find(5678){ user }
Then when the controller called User.find(1234), it would not return the user
we gave to the mock, since 1234 didn't match 5678... so it would still go to
database for the user which id is 1234. Suppose there's no error raised from
the controller, then before the test case ended, the rr adapter should call
RR.verify, and it would check if all mocks satisfied the requested methods.
Remember what is a mock?
4. mock - basically a stub but it also monitors program execution flow.
When I say "monitors program execution flow", I mean that it will check if
User.find(5678) is actually called once, and only once. In the above example,
the controller was calling User.find(1234), and never called User.find(5678),
that is, RR.verify would report an error that User.find(5678) was never called.
If it is called twice, it will report that it is expected to be called once,
but twice. If we want to say that User.find(5678) should be called twice,
we'll write:
mock(User).find(5678).times(2){ user }
This way, RR.verify would check if User.find(5678) was really called exactly
twice, not once nor never. As for stubs? just a mock that skips this times check.
stub(User).find(5678){ user }
This way, RR.verify would not check the times User.find(5678) called,
but just returned user every time User.find(5678) was called.
So what's a mock with proxy? Say we have this:
mock.proxy(User).find(5678)
This means when we call User.find(5678), it will really go to database to
find out the user. So basically it won't change the semantic of User.find,
but, since it is still a mock, so RR.verify would check if it was called
exactly once in this case too. Moreover, if we want to modify the object
which User.find(5678) returns, we can say this:
mock.proxy(User).find(5678){ |user| user.name = 'Am Am'; user }
Then when User.find(5678) got called, it will return the real user,
but, the name would be changed into 'Am Am'. This could be applied
to stub as well:
stub.proxy(User).find(5678){ |user| user.name = 'Am Am'; user }
You know the difference now. Stubs won't checked the times it is called.
Since any object could be injected, sometimes we'll do this:
mock(o = Object.new).to_s{ 'I am a monk.' }
puts o # => 'I am a monk.'
If we don't want to mix real objects, say User in the above example.
Mock could be very complex as well, see this example in rr's README:
mock.proxy(User).find('5') do |bob|
mock.proxy(bob).projects do |projects|
projects[0..3]
end
mock(bob).valid? {false}
bob
end
This means the next time User.find('5') is called, it will return bob, which is
also injected that when bob.projects is called, only returns the top 4 (the
README says it's 3, but 0, 1, 2, 3 is actually 4 elements.) projects.
Moreover, when bob.valid? is called, it is false. We can keep nesting
mocks, creating very, very complex mocks.
-
OK, so where do we want mocks or stubs?
1. fixtures.
2. things that already tested somewhere, and it is so slow to run.
3. reduce test case dependency to make it more "unit" and isolated.
For 1, I guess I don't need to explain too much, it's so easy to create fixtures
in rr if all what we want is very simple. For 3, it is always good to be lightweight.
As for 2, the best example would be Internet connection, since it's crazy slow
compare to our local machines, and it is usually tested on its own, so just leave
it alone. And there's webmock and fakeweb to do this job, not necessary to
jump into the HTTP client library or something like that.
Use mocks properly can reduce tons of asserts and/or shoulds, and it will be
much more clear to say how a program executes in an imperative world...
cheers,
0 retries:
Post a Comment
Note: Only a member of this blog may post a comment.