Muack Build Status

by Lin Jen-Shin (godfat)

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.

``` ruby 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.

ruby view = controller.template mock(view).render(:partial => "user_info") {"Information"}

There’s no twice modifier in Muack, use times(2) instead.

ruby 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.

ruby 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.

ruby 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.

ruby 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:

ruby 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.

ruby view = controller.template stub(view).render(:partial => "user_info") do |html| html.should include("Joe Smith") html end.proxy

Or use returns:

ruby 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.

ruby 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:

``` ruby subject = Object.new stub(subject).foo(1) subject.foo(1)

spy(subject).foo(1) spy(subject).bar # 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.

ruby 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:

ruby stub(object).foo { stub.bar{ :baz }.object } object.foo.bar #=> :baz

And of course the verbose way:

ruby bar = stub.bar{ :baz }.object stub(object).foo { bar } object.foo.bar #=> :baz

Or even more verbose, of course:

ruby bar = Object.new stub(bar).bar{ :baz } stub(object).foo { bar } object.foo.bar #=> :baz

Modifier

After defining a mock method, you get a Muack::Modifier back.

ruby 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.

ruby 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:

ruby 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.

ruby stub(object).method_missing(:[], is_a(Fixnum)){ |a| a+1 } object[1] #=> 2

Instead you can do this with returns:

ruby 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.

ruby 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:

ruby 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.

ruby 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.

ruby 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.

ruby mock(object).foo(1, 2) object.foo(1, 2) # ok object.foo(3) # fails

Passing no arguments really means passing no arguments.

ruby 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.

ruby 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 :)

ruby stub(object).foo object.foo # ok object.foo(1) # fails

Expecting method to never be called

Simply use times(0).

ruby mock(object).foo.times(0) object.foo # fails

Multiple mock with different argument set is fine, too.

ruby 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.

ruby 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.

ruby 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.

ruby 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:

ruby 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:

ruby 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.

ruby stub(object).foo object.foo object.foo object.foo

Argument wildcard matchers

anything is the same as RR.

ruby mock(object).foobar(1, anything) object.foobar(1, :my_symbol)

is_a is the same as RR.

ruby mock(object).foobar(is_a(Time)) object.foobar(Time.now)

No numeric supports. Simply use is_a(Numeric)

ruby mock(object).foobar(is_a(Numeric)) object.foobar(99)

No boolean supports, but you can use union (|).

ruby mock(object).foobar(is_a(TrueClass) | is_a(FalseClass)) object.foobar(false)

Or simply pass a custom satisfy block for it. Though there’s not much point here. Just want to demonstrate.

ruby mock(object).foobar( satisfy{ |a| a.kind_of?(TrueClass) || a.kind_of?(FalseClass) }) object.foobar(false)

Since duck_type is a weird name to me. Here we use respond_to(:walk, :talk).

ruby mock(object).foobar(respond_to(:walk, :talk)) arg = Object.new def arg.walk; 'waddle'; end def arg.talk; 'quack'; end object.foobar(arg)

You can also use intersection (&) for multiple responses. Though there’s not much point here. Just want to demonstrate.

ruby mock(object).foobar(respond_to(:walk) & respond_to(:talk)) arg = Object.new def arg.walk; 'waddle'; end def arg.talk; 'quack'; end object.foobar(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?

ruby mock(object).foobar(within(1..10)) object.foobar(5)

The same goes to regular expression. Use match instead.

ruby mock(object).foobar(match(/on/)) object.foobar("ruby on rails")

hash_including is the same as RR.

ruby mock(object).foobar(hash_including(:red => "#FF0000", :blue => "#0000FF")) object.foobar({:red => "#FF0000", :blue => "#0000FF", :green => "#00FF00"})

satisfy is the same as RR.

ruby mock(object).foobar(satisfy {|arg| arg.length == 2 }) object.foobar("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.

``` ruby 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.