Muack 
by Lin Jen-Shin (godfat)
LINKS:
DESCRIPTION:
Muack -- Yet another mocking library.
Basically it's an RR clone, but much faster under heavy use. It's 32x times faster (750s vs 23s) for running Rib tests.
REQUIREMENTS:
- Tested with MRI (official CRuby) 1.9.3, 2.0.0, Rubinius and JRuby.
INSTALLATION:
gem install muack
SYNOPSIS:
Basically it's an RR clone. Let's see a Bacon example.
require 'bacon'
require 'muack'
include Muack::API
describe 'Hello' do
before{ Muack.reset }
after { Muack.verify }
should 'say world!' do
str = 'Hello'
mock(str).say('!'){ |arg| "World#{arg}" }
str.say('!').should.equal 'World!'
end
end
Coming from RR?
Basically since it's an RR clone, the APIs are much the same. Let's see what's the different with code snippets. All codes were extracted from RR's API document.
mock
mock
is the same as RR.
view = controller.template
mock(view).render(:partial => "user_info") {"Information"}
There's no twice
modifier in Muack, use times(2)
instead.
mock(view).render.with_any_args.times(2) do |*args|
if args.first == {:partial => "user_info"}
"User Info"
else
"Stuff in the view #{args.inspect}"
end
end
stub
stub
is the same as RR.
jane = User.new
bob = User.new
stub(User).find('42') {jane}
stub(User).find('99') {bob}
stub(User).find do |id|
raise "Unexpected id #{id.inspect} passed to me"
end
times(0)
There's no dont_allow method in Muack, use times(0)
instead.
User.find('42').times(0)
User.find('42') # raises a Muack::Unexpected
proxy
Instead of calling proxy
immediately after calling mock
, we put
proxy
the last because it's a method from Muack::Modifier
.
view = controller.template
mock(view).render(:partial => "right_navigation").proxy
mock(view).render(:partial => "user_info") do |html|
html.should include("John Doe")
"Different html"
end.proxy
If you feel it is weird to put proxy the last, you can also use
returns
modifier to put the block last as this:
view = controller.template
mock(view).render(:partial => "right_navigation").proxy
mock(view).render(:partial => "user_info").proxy.returns do |html|
html.should include("John Doe")
"Different html"
end
The same goes to stub
.
view = controller.template
stub(view).render(:partial => "user_info") do |html|
html.should include("Joe Smith")
html
end.proxy
Or use returns
:
view = controller.template
stub(view).render(:partial => "user_info").proxy.returns do |html|
html.should include("Joe Smith")
html
end
any_instance_of
Only this form of any_instance_of
is supported. On the other hand,
any of the above is supported as well, not only stub.
any_instance_of(User) do |u|
stub(u).valid? { false }
mock(u).errors { [] }
mock(u).save.proxy
stub(u).reload.proxy
end
Spies
We don't try to provide different methods for different testing framework, so that we don't have to create so many testing framework adapters, and try to be smart to find the correct adapter. There are simply too many testing frameworks out there. Ruby's built-in test/unit and minitest have a lot of different versions, so does rspec.
Here we just try to do it the Muack's way:
subject = Object.new
stub(subject).foo(1)
subject.foo(1)
spy(subject).foo(1)
spy(subject). # This doesn't verify immediately.
Muack.verify # This fails, saying `bar` was never called.
Block form
Block form is also supported. However we don't support instance_eval
form.
There's little point to use instance_eval since it's much more complicated
and much slower.
script = MyScript.new
mock(script) do |expect|
expect.system("cd #{RAILS_ENV}") {true}
expect.system("rake foo:bar") {true}
expect.system("rake baz") {true}
end
Nested mocks
The shortest API (which might be a bit tricky) is not supported, but we do support:
stub(object).foo { stub.{ :baz }.object }
object.foo. #=> :baz
And of course the verbose way:
= stub.{ :baz }.object
stub(object).foo { }
object.foo. #=> :baz
Or even more verbose, of course:
= Object.new
stub().{ :baz }
stub(object).foo { }
object.foo. #=> :baz
Modifier
After defining a mock method, you get a Muack::Modifier
back.
stub(object).foo #=> Muack::Modifier
However, you cannot flip around methods like RR. Whenever you define a mock/stub method, you must provide the block immediately.
mock(object).foo{ 'bar' }.times(2)
If unfortunately, the method name you want to mock is already defined,
you can call method_missing
directly to mock it. For example, inspect
is already defined in Muack::Mock
to avoid crashing with Bacon.
In this case, you should do this to mock inspect
:
mock(object).method_missing(:inspect){ 'bar' }.times(2)
Stubbing method implementation / return value
Again, we embrace one true API to avoid confusion, unless the alternative
API really has a great advantage. So we encourage people to use the block to
return values. However, sometimes you cannot easily do that for certain
methods due to Ruby's syntax. For example, you can't pass a block to
a subscript operator []
. As a workaround, you can do it with
method_missing
, though it's not very obvious if you don't know
what is method_missing
.
stub(object).method_missing(:[], is_a(Fixnum)){ |a| a+1 }
object[1] #=> 2
Instead you can do this with returns
:
stub(object)[is_a(Fixnum)].returns{ |a| a + 1 }
object[1] #=> 2
You can also pass a value directly to returns
if you only want to return
a simple value.
stub(object)[is_a(Fixnum)].returns(2)
object[1] #=> 2
On the other hand, since Muack is more strict than RR. Passing no arguments means you really don't want any argument. Here we need to specify the argument for Muack. The example in RR should be changed to this in Muack:
stub(object).foo(is_a(Fixnum), anything){ |age, count, &block|
raise 'hell' if age < 16
ret = block.call count
blue? ? ret : 'whatever'
}
Stubbing method implementation based on argument expectation
Here is exactly the same as RR.
stub(object).foo { 'bar' }
stub(object).foo(1, 2) { 'baz' }
object.foo #=> 'bar'
object.foo(1, 2) #=> 'baz'
Stubbing method to yield given block
Always use the block to pass whatever back.
stub(object).foo{ |&block| block.call(1, 2, 3) }
object.foo {|*args| args } # [1, 2, 3]
Expecting method to be called with exact argument list
Muack is strict, you always have to specify the argument list.
mock(object).foo(1, 2)
object.foo(1, 2) # ok
object.foo(3) # fails
Passing no arguments really means passing no arguments.
stub(object).foo
stub(object).foo(1, 2)
object.foo(1, 2) # ok
object.foo # ok
object.foo(3) # fails
Expecting method to be called with any arguments
Muack also provides with_any_args
if we don't really care.
stub(object).foo.with_any_args
object.foo # ok
object.foo(1) # also ok
object.foo(1, 2) # also ok
# ... you get the idea
Expecting method to be called with no arguments
Just don't pass any argument :)
stub(object).foo
object.foo # ok
object.foo(1) # fails
Expecting method to never be called
Simply use times(0)
.
mock(object).foo.times(0)
object.foo # fails
Multiple mock with different argument set is fine, too.
mock(object).foo(1, 2).times(0)
mock(object).foo(3, 4)
object.foo(3, 4) # ok
object.foo(1, 2) # fails
Expecting method to be called only once
By default, a mock only expects a call. Using times(1)
is actually a no-op.
mock(object).foo.times(1)
object.foo
object.foo # fails
Expecting method to called exact number of times
Times! Which is the same as RR.
mock(object).foo.times(3)
object.foo
object.foo
object.foo
object.foo # fails
Alternatively, you could also do this. It's exactly the same.
3.times{ mock(object).foo }
object.foo
object.foo
object.foo
object.foo # fails
Expecting method to be called minimum number of times
It's not supported in Muack, but we could emulate it somehow:
times = 0
stub(object).foo{ times += 1 }
object.foo
object.foo
raise "BOOM" if times <= 3
Expecting method to be called maximum number of times
It's not supported in Muack, but we could emulate it somehow:
times = 0
stub(object).foo{ times += 1; raise "BOOM" if times > 3 }
object.foo
object.foo
object.foo
object.foo
Expecting method to be called any number of times
Just use stub
, which is exactly why it is designed.
stub(object).foo
object.foo
object.foo
object.foo
Argument wildcard matchers
anything
is the same as RR.
mock(object).(1, anything)
object.(1, :my_symbol)
is_a
is the same as RR.
mock(object).(is_a(Time))
object.(Time.now)
No numeric supports. Simply use is_a(Numeric)
mock(object).(is_a(Numeric))
object.(99)
No boolean supports, but you can use union (|
).
mock(object).(is_a(TrueClass) | is_a(FalseClass))
object.(false)
Since duck_type is a weird name to me. Here we use respond_to(:walk, :talk)
.
mock(object).(respond_to(:walk, :talk))
arg = Object.new
def arg.walk; 'waddle'; end
def arg.talk; 'quack'; end
object.(arg)
You can also use intersection (&
) for multiple responses.
Though there's not much point here. Just want to demonstrate.
mock(object).(respond_to(:walk) & respond_to(:talk))
arg = Object.new
def arg.walk; 'waddle'; end
def arg.talk; 'quack'; end
object.(arg)
Don't pass ranges directly for ranges, use within
. Or how do we tell
if we really want the argument to be a Range
object?
mock(object).(within(1..10))
object.(5)
The same goes to regular expression. Use match
instead.
mock(object).(match(/on/))
object.("ruby on rails")
hash_including
is the same as RR.
mock(object).(hash_including(:red => "#FF0000", :blue => "#0000FF"))
object.({:red => "#FF0000", :blue => "#0000FF", :green => "#00FF00"})
satisfy
is the same as RR.
mock(object).(satisfy {|arg| arg.length == 2 })
object.("xy")
Writing your own argument matchers
See lib/muack.rb
and lib/muack/satisfy.rb
,
you would get the idea soon. Here's how is_a
implemented.
module Muack::API
module_function
def is_a klass
Muack::IsA.new(klass)
end
end
class Muack::IsA < Muack::Satisfy
def initialize klass
super lambda{ |actual_arg| actual_arg.kind_of?(klass) }, [klass]
end
end
USERS:
CONTRIBUTORS:
- Lin Jen-Shin (@godfat)
LICENSE:
Apache License 2.0
Copyright (c) 2013, 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.