Muack
by Lin Jen-Shin (godfat)
LINKS:
DESCRIPTION:
Muack -- A fast, small, yet powerful mocking library.
Inspired by RR, and it's 32x times faster (750s vs 23s) than RR
for running Rib tests.
WHY?
Because RR has/had some bugs and it is too complex for me to fix it.
Muack is much simpler and thus much faster and much more consistent.
REQUIREMENTS:
- Tested with MRI (official CRuby) and JRuby.
INSTALLATION:
gem install muack
SYNOPSIS:
Here's a quick example using Pork.
require 'pork/auto'
require 'muack'
describe 'Hello' do
include Muack::API
before{ Muack.reset }
after { Muack.verify }
would 'say world!' do
str = 'Hello'.dup
mock(str).say('!'){ |arg| "World#{arg}" }
str.say('!').should.eq 'World!'
end
end
Overview
There are 3 parts in Muack, which are:
Mocks are objects with injected methods which we could observe, and mocks
modifiers are telling how we want to observe the mocks, and finally argument
verifiers could help us observe the arguments passed to the injected methods.
Let's explain them one by one.
Mocks
There are also 4 different kinds of mocks in Muack, which are:
You could think of mocks are sort of stubs combined with spies.
Here's the inequation: (we'll talk about coats later)
mock >= stub + spy
Stubs help us inject methods into the objects we want to observe. Spies
help us observe the behaviours of the objects. As for mocks, they inject
methods and observe the behaviours in realtime. They complain immediately
if the behaviours were unexpected. In contrast, if we're not asking spies,
stubs won't complain themselves.
Here's an example using a mock:
obj = Object.new
mock(obj).name{ 'obj' }
p obj.name
p Muack.verify
Which is similar to using a stub with a spy:
obj = Object.new
stub(obj).name{ 'obj' }
p obj.name
spy(obj).name
p Muack.verify
You might wonder, then why mocks or why stubs with spies? The advantage of
using mocks is that, you only need to specify once. I guess this is quite
obvious. However, sometimes we don't care if the injected methods are called
or not, but sometimes we do care. With stubs and spies, we could always put
stubs in the before/setup block, and only when we really care if they are
called or not, we put spies to examine.
The other difference is that, spies could partially verify the corresponding
stubs, but not necessarily completely as mocks. For example, we could stub
two methods, but only verify one of them with a spy.
obj = Object.new
stub(obj).name{ 'obj' }
stub(obj).id { 12345 }
p obj.name
p obj.id
spy(obj).name
p Muack.verify
This is similar as mixing mocks and stubs, as in the following example:
obj = Object.new
stub(obj).name{ 'obj' }
mock(obj).id { 12345 }
p obj.name
p obj.id
p Muack.verify
However you should not mix mocks and stubs with the same method, or you
might encounter some unexpected result. Jump to Caveat for more detail.
The other differences for stubs and spies, please check
Pattern Matching for stubs and spies.
In short, stubs and spies would do some kind of pattern matching,
making the order of the same method irrelevant.
On the other hand, stubs aren't limited to testing. If we want to monkey
patching something, stubs could be useful as we don't care how many times
the injected methods are called. Jump to Muack as a mocky patching library
section for more detail.
reset and verify
Calling Muack.reset
is essentially resetting all mocks, returning all
objects/classes back to their original states. In the very first example,
we do this in a before block to make sure that we're in a clean state.
Calling Muack.verify
is essentially verifying if all mocks and spies are
satisfied, if so, it would return true; otherwise, raising an exception.
Then, no matter verification passed or not, Muack would reset itself.
That means we don't really need to call Muack.reset
in a before block if
we're pretty sure that all test cases would call Muack.verify
in the end,
resetting everything.
On the other hand, we could also reset or verify a single object without
affecting the others. This is helpful in the cases of mocking some very
basic objects like Time, without causing too much side effect.
name = 'str'.dup
stub(name).to_s{ 'hi' }
stub(Time).new { Time.at(0) }
mock(Time).now { Time.new }
p name.to_s
p Time.now.to_i
p Time.new.to_i
p Muack.verify(Time)
p name.to_s
p Time.now.to_i > 0
p Time.new.to_i > 0
Muack.reset(name)
p name.to_s
p Muack.verify
Coats
Now we could talk about coats. It's a kind of mocks but it would wear out
instead of raising an exception when it's called more than expected times.
This is useful when we want to restore the original behaviour of a
particular method at some point. The problem is that we can't simply
call the original method because it's already mocked! We could workaround
this by using Muack.verify
or Muack.reset
at some point, or let coats
handle that.
Here's an example with coats:
coat(Time).now{ Time.at(0) }.times(2)
p Time.now.to_i == 0
p Time.now.to_i == 0
p Time.now.to_i > 0
p Muack.verify
Without coats we might end up with:
mock(Time).now{ Time.at(0) }
mock(Time).now{ Muack.verify(Time); Time.at(0) }
p Time.now.to_i == 0
p Time.now.to_i == 0
p Time.now.to_i > 0
p Muack.verify
Anonymous mode
Sometimes we just want to stub something without a concrete object in mind.
By calling mock
or stub
without any argument, we're creating an anonymous
mock/stub. This is because the default argument for mock
and stub
is just
Object.new
.
But how do we access the anonymously created object? We'll use the object
method on the modifier to access it. Here's an example:
obj = mock.name{ 'obj' }.object
p obj.name
p Muack.verify
This is exactly equivalent to this:
mock(obj = Object.new).name{ 'obj' }
p obj.name
p Muack.verify
Also, if we want to mock over multiple methods, we could also take the
advantage of block form of mock
and stub
method.
obj = mock{ |m|
m.name{ 'obj' }
m.id { 12345 }
}.object
p obj.name
p obj.id
p Muack.verify
We can't omit the object
method here because after defining the injected
method, we'll get a modifier to describe the properties of the injected
method. Jump to Mocks Modifiers for details.
Proxy mode
There are chances that we don't really want to change the underlying
implementation for a given method, but we still want to make sure the
named method is called, and that's what we're testing for.
In those cases, proxy mode would be quite helpful. To turn a mock or stub
into proxy mode we simply do not provide any block to the injected method,
but just name it. Here's an example:
str = 'str'.dup
mock(str).reverse
p str.reverse
p Muack.verify
Note that if reverse was not called exactly once, the mock would complain.
We could also use stub + spy to do the same thing as well:
str = 'str'.dup
stub(str).reverse
p str.reverse
spy(str).reverse
p Muack.verify
You might also want to use peek_args
and peek_return
modifier along with
proxies in order to slightly tweak the original implementation. Jump to
Muack as a mocky patching library section for more detail.
Partial mode
Occasionally we would want to fake some of the values inside a hash, but
we don't want to interfere with the other values in that hash, and we also
don't want to modify it directly, or we'll need to make sure to restore it
after the tests.
Partial mode is not really a mode, but a combination of using proxy mode and
the pattern matching mechanism specialized in stubs.
Suppose we want to stub ENV
(which is not a hash but you get the idea),
enabling some of the flags inside tests without really setting it, we'll do:
@user = ENV['USER']
p ENV['NDEBUG']
stub(ENV)[is_a(String)]
stub(ENV)['NDEBUG'].returns{ '1' }
p ENV['NDEBUG']
p ENV['USER']
p Muack.verify
p ENV['NDEBUG']
Note that in order to make this work, proxy should be defined first. Because
stubs are searched in Last In First Out (LIFO) order, it would first check
if the key is matching 'NDEBUG'
in this case. If it's not matched, then
search the next one. Eventually it would reach to the first stub, which
we put is_a(String)
there so it must match, and return the original value
inside ENV
.
If the order is reversed, then it would always return the original value,
because the proxy would always match, and Muack would stop searching the
next stub.
any_instance_of mode
We only talked about mocking a specific object, but never mentioned what if
the objects we want to mock aren't at hand at the time we define mocks?
In those cases, instead of trying to mock object creation and return the
mock we defined, we might want to simply mock any instance of a particular
class, since this would make the process much easier.
Here we could use a special "mock" called any_instance_of
, which takes a
class and returns a Muack::AnyInstanceOf
which represents the instance of
the class we just passed. Having this special representation, we could treat
it as if a real instance and define regular mocks/stubs on it. It would then
applies to any instance of the class we gave.
Example speaks:
array = any_instance_of(Array)
stub(array).name{ 'array' }
p [ ].name
p [0].name
p Muack.verify
And as most of the time we don't care about the representation after mocks
were defined, we could use the block form:
any_instance_of(Array) do |array|
stub(array).name{ 'array' }
stub(array).id { 1234567 }
end
p [ ].name
p [0].id
p Muack.verify
Note that if you need to access the real instance instead of the
representation in the injected method, you might want to enable
instance_exec mode. Please jump to instance_exec mode section
for more detail.
Here's an quick example:
any_instance_of(Array) do |array|
p array.class
mock(array).name.returns(:instance_exec => true){ inspect }
end
p [0, 1].name
p Muack.verify
Lastly, you could also use any_instance_of
along with proxy mode,
or any other combination you could think of:
any_instance_of(Array) do |array|
stub(array).name{ 'array' }
mock(array).max
end
p [ ].name
p [0].max
p Muack.verify
Though you should still not mix mocks and stubs with the same method,
and as you could tell from the above example, Muack would not complain
for every array without calling max
once. This is because any_instance_of
would count on all instances, instead of individual instances. Here
we're actually telling Muack that max
should be called exactly once
amongst all instances of array, and it is indeed called exactly once
amongst two instances here.
This might or might not be what we want. But think it twice, if we're
mocking any instance of a very basic class in Ruby, testing against
individual instances could be too strict since it's used everywhere!
Please check Caveat section for more details.
Mocks Modifiers
A modifier is something specifying a property of an injected method.
By making a mock/stub/spy, it would return a modifier descriptor which
we could then specify properties about the injected method.
Note that we could chain properties for a given modifier descriptor
because all public methods for declaring a property would return the
modifier descriptor itself. Let's see the specific usages for each
properties with concrete examples.
times
By using mocks, we are saying that the injected method should be called
exactly once. However the injected method might be called more than once,
say, twice. We could specify this with times
modifier:
obj = Object.new
mock(obj).name{ 'obj' }.times(2)
p obj.name
p obj.name
p Muack.verify
This is actually also semantically equivalent to making the mock twice:
obj = Object.new
mock(obj).name{ 'obj' }
mock(obj).name{ 'obj' }
p obj.name
p obj.name
p Muack.verify
Note that it does not make sense to specify times
for stubs, because
stubs don't care about times. Spies do, though. So this is also
similar to below:
obj = Object.new
stub(obj).name{ 'obj' }
p obj.name
p obj.name
spy(obj).name.times(2)
p Muack.verify
Or without using times for spy:
obj = Object.new
stub(obj).name{ 'obj' }
p obj.name
p obj.name
spy(obj).name
spy(obj).name
p Muack.verify
The advantage of specifying mocks twice is that we could actually provide
different results for each call. You could think of it as a stack. Here's
a simple example:
obj = Object.new
mock(obj).name{ 0 }
mock(obj).name{ 1 }
mock(obj).name{ 2 }
p obj.name
p obj.name
p obj.name
p Muack.verify
We could also use the block form for convenience:
obj = Object.new
mock(obj) do |m|
m.name{ 0 }
m.name{ 1 }
m.name{ 2 }
end
p obj.name
p obj.name
p obj.name
p Muack.verify
Note that this does not apply to stubs because stubs never run out. Instead,
the latter stub would overwrite the previous one.
obj = Object.new
stub(obj) do |m|
m.name{ 0 }
m.name{ 1 }
m.name{ 2 }
end
p obj.name
p obj.name
p obj.name
p Muack.verify
Note that if you do not want a given method be called at all, you could
use times(0)
to enforce this.
with_any_args
We haven't talked about verifying arguments. With with_any_args
modifier,
we're saying that we don't care about the arguments. If we're not specifying
any arguments like above examples, we're saying there's no arguments at all.
Here we'll show an example for with_any_args
. If you do want to verify some
specific arguments, jump to Arguments Verifiers section.
obj = Object.new
mock(obj).name{ 'obj' }.with_any_args.times(4)
p obj.name
p obj.name(1)
p obj.name(nil)
p obj.name(true)
p Muack.verify
returns
For some methods, we can't really pass a block to specify the implementation.
For example, we can't pass a block to []
, which is a Ruby syntax limitation.
To workaround it, we could use returns
property:
obj = Object.new
mock(obj)[0].returns{ 0 }
p obj[0]
p Muack.verify
This is also useful when we want to put the implementation block in the last
instead of the beginning. Here's an example:
obj = Object.new
mock(obj).name.times(2).with_any_args.returns{ 'obj' }
p obj.name
p obj.name
p Muack.verify
On the other hand, there's also another advantage of using returns
than
passing the block directly to the injected method. With returns
, there's
an additional option we could use by passing arguments to returns
. We
can't do this in regular injected method definition because those arguments
are for verifying the actual arguments. Jump to Arguments Verifiers section
for details.
The only option right now is :instance_exec
.
instance_exec mode
By default, the block passed to the injected method is lexically/statically
scoped. That means, the scope is bound to the current binding. This is the
default because usually we don't need dynamic scopes, and we simply want to
return a plain value, and this is much easier to understand, and it is the
default for most programming languages, and it would definitely reduce
surprises. If we really need to operate on the object, we have it, and
we could touch the internal by calling instance_eval on the object.
However, things are a bit different if we're using any_instance_of
.
If we're using any_instance_of
, then we don't have the instance at
hand at the time we're defining the block, but only a Muack::AnyInstanceOf
instance to represent the instance. There's no way we could really touch
the object without instance_exec
option.
This would also be extremely helpful if we're using Muack as a monkey
patching library. We don't have to copy the original codes in order to
monkey patching a class, we could simply inject what we really want to
fix the internal stuffs in the broken libraries we're using. Jump to
Muack as a mocky patching library section for more detail.
Here's an quick example:
any_instance_of(Array) do |array|
p array.class
mock(array).name.returns(:instance_exec => true){ inspect }
end
p [0, 1].name
p Muack.verify
Note that this :instance_exec
option also applies to other modifiers which
accepts a block for its implementation, i.e. peek_args
and peek_return
.
peek_args
What if we don't really want to change an underlying implementation for a
given method, but we just want to slightly change the arguments, or we
might just want to take a look at the arguments? Here's an example using
peek_args
to modify the original arguments.
Note that here we use the proxy mode for the mock, because if we're defining
our own behaviour, then we already have full control of the arguments.
There's no points to use both. This also applies to peek_return
.
str = 'ff'.dup
mock(str).to_i.with_any_args.peek_args{ |radix| radix * 2 }
p str.to_i(8)
p Muack.verify
peek_args
also supports :instance_exec
mode. Here's an example:
any_instance_of(Array) do |array|
stub(array).push.with_any_args.
peek_args(:instance_exec => true){ |_| size }
end
a = []
p a.push.dup
p a.push.dup
p a.push.dup
p Muack.verify
We could also omit |_|
if we don't care about the original argument
in the above example.
peek_return
What if we don't really want to change an underlying implementation for a
given method, but we just want to slightly change the return value, or we
might just want to take a look at the return? Here's an example using
peek_return
to modify the original return value.
str = 'ff'.dup
mock(str).to_i.with_any_args.peek_return{ |int| int * 2 }
p str.to_i(16)
p Muack.verify
peek_return
also supports :instance_exec
mode. Here's an example:
any_instance_of(Array) do |array|
stub(array).push.with_any_args.
peek_return(:instance_exec => true){ |_| size }
end
a = []
p a.push(0)
p a.push(0)
p a.push(0)
p a
p Muack.verify
We could also omit |_|
if we don't care about the original return value
in the above example.
Arguments Verifiers (Satisfying)
If we're not passing any arguments to the injected method we define, then
basically we're saying that there's no arguments should be passed to the
method. If we don't care about the arguments, then we should use
with_any_args
modifier. If we want the exact arguments, then we
should just pass the arguments, which would be checked with ==
operator.
Here's an example:
obj = Object.new
mock(obj).say('Hi'){ |arg| arg }
p obj.say('Hi')
p Muack.verify
This also applies to multiple arguments:
obj = Object.new
mock(obj).say('Hello', 'World'){ |*args| args.join(', ') }
p obj.say('Hello', 'World')
p Muack.verify
We could also retrieve the block argument:
obj = Object.new
mock(obj).say{ |&block| block.call('Hi') }
obj.say{ |msg| p msg }
p Muack.verify
Pattern Matching for stubs and spies
Moreover, we could also have stubs on the same method for different
arguments. We could think of this as a sort of pattern matching, and Muack
would try to find the best matched stub for us.
obj = Object.new
stub(obj).find(0){ 0 }
stub(obj).find(1){ 1 }
p obj.find(1)
p obj.find(0)
p Muack.verify
If obj.find(2)
is called and Muack cannot find a matched stub, it would
raise a Muack::Unexpected
and list the candidates for us. This also
applies to spies.
However, What if we don't want to be so exact? Then we should use verifiers.
We'll introduce each of them in next section. Note that verifiers
are not recursive though. If you need complex arguments verification,
you'll need to use satisfy
verifier which you could give an arbitrary
block to verify anything.
anything
anything
is a wildcard arguments verifier. It matches anything.
Although this actually verifies nothing, we could still think of
this as an arity verifier. Since one anything is not two anythings.
obj = Object.new
mock(obj).say(anything){ |arg| arg }.times(2)
p obj.say(0)
p obj.say(true)
p Muack.verify
is_a
is_a
would check if the argument is a kind of the given class.
Actually, it's calling kind_of?
underneath.
obj = Object.new
mock(obj).say(is_a(String)){ |arg| arg }
p obj.say('something')
p Muack.verify
matching
matching
would check the argument with match
method. Usually this is
used with regular expression, but anything which responds to match
should work.
obj = Object.new
mock(obj).say(matching(/\w+/)){ |arg| arg }
p obj.say('Hi')
p Muack.verify
Note that please don't pass the regular expression directly without
wrapping it with a match verifier, or how do we distinguish if we
really want to make sure the argument is exactly the regular expression?
including
including
would check if the actual argument includes the given value
via include?
method.
obj = Object.new
mock(obj).say(including(0)){ |arg| arg }
p obj.say([0,1])
p Muack.verify
within
within
is the reverse version of including
, verifying if the actual
argument is included in the given value.
obj = Object.new
mock(obj).say(within([0, 1])){ |arg| arg }
p obj.say(0)
p Muack.verify
responding_to
responding_to
would check if the actual argument would be responding to
the given message, checked via respond_to?
, also known as duck typing.
obj = Object.new
mock(obj).say(responding_to(:size)){ |arg| arg }
p obj.say([])
p Muack.verify
Note that you could give multiple messages to responding_to
.
obj = Object.new
mock(obj).say(responding_to(:size, :reverse)){ |arg| arg }
p obj.say([])
p Muack.verify
where
where
would check if the actual argument matches given specification.
obj = Object.new
mock(obj).say(where(:a => is_a(Integer))){ |arg| arg }
p obj.say(:a => 0)
p Muack.verify
Note that this could be recursive.
obj = Object.new
mock(obj).say(where(:a => {:b => [is_a(Integer)]})){ |arg| arg[:a] }
p obj.say(:a => {:b => [0]})
p Muack.verify
having
having
would check if the actual argument is a superset of given
specification.
obj = Object.new
mock(obj).say(having(:a => 0)){ |arg| arg }
p obj.say(:a => 0, :b => 1)
p Muack.verify
Note that this could be recursive.
obj = Object.new
mock(obj).say(having(:a => {:b => [is_a(Integer)]})){ |arg| arg[:c] }
p obj.say(:a => {:b => [1]}, :c => 2)
p Muack.verify
allowing
allowing
would check if the actual argument is a subset of given
specification.
obj = Object.new
mock(obj).say(allowing(:a => 0, :b => [1])){ |arg| arg }
p obj.say(:a => 0)
p Muack.verify
Note that this could be recursive.
obj = Object.new
mock(obj).say(allowing(:a => {:b => is_a(Integer), :c => 1})){ |arg| arg[:a] }
p obj.say(:a => {:b => 2})
p Muack.verify
satisfying
satisfying
accepts a block to let you do arbitrary verification.
nil and false are considered false, otherwise true, just like in
regular if expression.
obj = Object.new
mock(obj).say(satisfying{ |arg| arg % 2 == 0 }){ |arg| arg }
p obj.say(0)
p Muack.verify
Disjunction (|)
If what we want is the actual argument be within either 0..1
or 3..4
?
We don't really have to use satisfy
to build custom verifier, we could
compose verifiers with disjunction operator (|).
obj = Object.new
mock(obj).say(within(0..1) | within(3..4)){ |arg| arg }.times(2)
p obj.say(0) # 0
p obj.say(4) # 4
p Muack.verify # true
Or boolean, you might say:
obj = Object.new
mock(obj).say(is_a(TrueClass) | is_a(FalseClass)){ |arg| arg }.times(2)
p obj.say(true) # true
p obj.say(false) # false
p Muack.verify # true
Conjunction (&)
If what we want is the actual argument not only a kind of something,
but also responds to something. For example, an Enumerable requires the
class implements each method. We could use conjunction for this.
obj = Object.new
mock(obj).say(is_a(Enumerable) & responding_to(:each)){}.times(3)
p obj.say( [] )
p obj.say( {} )
p obj.say(0..1)
p Muack.verify
Caveat
Mixing mocks and stubs
We could and probably would also want to mix mocks and stubs, for example,
we might be concerned about some methods for a given object, but not the
other methods.
obj = Object.new
stub(obj).name{ 'obj' }
mock(obj).id { 12345 }
p obj.name
p obj.name
p obj.id
p Muack.verify
However, it might act unexpectedly if we mock and stub on the same object
for the same method. It would somehow act like the latter would always win!
So if we define mock later for the same method, previously defined stub
would never be called. On the other hand, if we define stub later for the
same method, previously defined mock would always complain because it would
never be called, either!
This does not mean previously defined mocks or stubs get overwritten, because
it would still take effect. It's just that there's no way they could get
called. So this is mostly not desired.
The ideal solution to this would be raising an error immediately, or really
make it could be overwritten. However I didn't find a good way to handle this
without rewriting the internal details. So I'll just leave it as it is,
and hope no one would ever try to do this.
any_instance_of shares all calls for a given class
We might assume that mocks with any_instance_of would work exactly the same
as regular mocks, but this is actually not the case. Regular mocks count
on every individual instance, but all instances share the same count for
any_instance_of.
With one instance:
any_instance_of(Array){ |array| mock(array).f{true}.times(2) }
a = []
p a.f
p a.f
p Muack.verify
With two instances:
any_instance_of(Array){ |array| mock(array).f{true}.times(2) }
p [].f
p [].f
p Muack.verify
So remember to count on all instances, but not individual ones.
Muack as a mocky patching library
Consider you're using a broken library and you need an immediate fix without
waiting for upstream to merge your patch, and release a new version.
You could fix it more elegantly by subclassing the original class, or try to
include or extend a module to make the original class work correctly. But
sometimes we just cannot do this because of the implementation. They might
not be extensible at all. Consider if there's a method contains 1,000
lines... There's no way to change it in the middle of the method other than
touching the lines directly, unless we have some line based AOP tools...
which is not really practical.
In this case, we could fork it and maintain everything by ourselves, and
merge from upstream occasionally. However we might only want to do this as
the last resort since this could cost a lot.
Alternatively, we can copy the original code, and put it somewhere, and
load it after the original code was loaded, so we have the patched and
correct code running. This is also called monkey patching, patching like a
monkey. Generally this is a bad idea, but sometimes we can only do this to
workaround some broken libraries. For example, some libraries might not be
maintained, or the authors refused to fix this due to other reasonable or
unreasonable reason.
The most notable drawback of monkey patching is that, we're copying a lot of
codes which could be changed upstream, and we might not be aware of that,
and update our monkey patch accordingly. This could cause some incompatible
issues.
That means, the fewer copied codes, the better. Muack could actually help
in this case. I called this mocky patching. The advantage of using this
technique is that, we have peek_args
and peek_return
which we could
modify the arguments or return values in runtime, without changing any
implementation of a particular method.
Here's a real world example with rails_admin. The problem in rails_admin is
that, it assumes every associated records should have already been saved,
thus having an id, and there's also a particular show page for it.
However, in our application, we could have associated records not yet saved
in the database. rails_admin would try to retrieve routes for those unsaved
records, and rails would raise RoutingError because rails_admin is passing
no id for a show path.
The idea of this fix is simple. Just don't try to get the show page for
records which are not yet saved, i.e. records without an id. However this
is actually extremely hard to fix in rails_admin without monkey patching!
I'll skip all those details and my rants. In the end, I fixed this by
trying to peek the arguments for a particular method, and if and only if
the passed records are not yet saved in the database, we fake the arguments.
Otherwise, we just bypass and fallback to the original implementation.
Here's the code:
Muack::API.stub(RailsAdmin::Config::Actions).find.with_any_args.
peek_args do |*args|
custom_key, bindings = args
if bindings && bindings[:object] && bindings[:object].id.nil?
[nil, {}]
else
args
end
end
If we don't do mocky patching but monkey patching, we'll end up with
copying the entire method for RailsAdmin::Config::Actions.find, which then,
we'll be responsible for updating this method if some of the original
implementation changed.
Note that in mocky patching, we should always use stub and never call
Muack.verify
or Muack.reset
, or that would defeat the purpose of
mocky patching.
Muack as a development runtime static typing system
Ever consider a static type system in Ruby? You could actually see a lot of
asserts inserted in the beginning of some methods in some libraries. For
example, there are assert_valid_key_size
, assert_kind_of
, etc, in
dm-core, and assert_valid_keys
, assert_valid_transaction_action
,
and various random asserts in activerecord.
You could find them by searching against raise ArgumentError
because
rails is much less consistent and sometimes it's hard to find a pattern in
rails. But you get the idea, those ArgumentError
would much help us debug
our code from misusing the API, and that's exactly the point of type system,
or more specifically, static type system.
We could also use some static analysis tools to do something like this, for
example, there's ruby-lint. However, as you might already know, since
Ruby is so dynamic, static analysis tools cannot really do a great job if
our code is quite dynamic. Of course we could write it more statically,
and treat our static analysis tools better, but that might not be the spirit
of Ruby somehow.
Alternatively, it would be great to do this static type checking
dynamically... I mean, in the runtime rather than compile time. This
means it would be much more accurate, just like those asserts in the
above examples.
However, if we're doing those checks in a hot path, for example, right
inside a loop looping over a million times, this would definitely slow
things down if we're checking them in the runtime. Even if we put $DEBUG
guards around those check, we're still suffering from checking the flag.
It would be great if we could actually just remove those checks in
production, while turn it on when we're developing or debugging.
Muack could actually fulfill this desire, as it could inject codes
externally and seamlessly, and we could remove them anytime when we
call Muack.reset
, or, simply don't do any stubs in production config.
Consider we have two classes:
Food = Class.new
User = Class.new(Struct.new(:food))
And we could make sure User#food is always a kind of Food
by putting this
into a development config or so:
Muack::API.module_eval do
any_instance_of(User) do |user|
stub(user).food = is_a(Food)
end
end
And then if we're trying to set a food other than a Food
...
u, f = User.new, Food.new
u.food = f
u.food = 1
This could go wild and we could customize our own domain specific argument
verifiers. For example, we could do this to check if the food is frozen:
Food = Class.new
User = Class.new(Struct.new(:food))
FoodFrozen = Class.new(Muack::Satisfying) do
def match actual_arg
actual_arg.frozen?
end
end
Muack::API.module_eval do
any_instance_of(User) do |user|
stub(user).food = FoodFrozen.new
end
end
u = User.new
p u.food = Food.new.freeze
p u.food = Food.new
Please check Arguments Verifiers (Satisfying) section for more argument
verifiers details.
Why didn't mocks nor stubs check if the injected method exists before?
Long story short. I can't find a set of good APIs along with good
implementation. My ideal APIs would be that for mocks and stubs, they
do check if the injected methods exist before, and if we don't want
that check, we use fake
instead of mock
or stub
.
However, how do we specify if fake
should act like mock
or stub
?
Introducing yet another name would make the terms even more confusing
(which are already fairly confusing!), and I don't want something like:
fake.mock
or mock.fake
or fake_mock
or mock_fake
. Using an option
would also raise the other questions.
What if we make mock.with_any_times
work exactly like stub
then?
Then we could have fake.with_any_times
and that would be the stub
version of fake. This should greatly reduce the complexity and confusion.
However this won't work well because stub is not just mock without times.
They are different in:
- Mocked methods are called in FIFO (queue) order
- Stubbed methods are called in FILO (stack) order
- Stubbed methods could do some pattern matching
Of course we could break them though, but do we really have to, just for
this simple feature? Also, it could be pretty challenging to implement
existing method checking for any_instance_of
.
If you could find a good set of APIs while implementing it nicely, please
do let me know. Compatibility is not an issue. We could always bump the
major number to inform this incompatibility. I am open to breaking legacy.
Or, I am happy to break legacy.
USERS:
CONTRIBUTORS:
LICENSE:
Apache License 2.0 (Apache-2.0)
Copyright (c) 2013-2022, Lin Jen-Shin (godfat)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.