Building A Simple Mocking Library in Ruby

This tutorial is based on Andy Lindeman's awesome talk — Building a Mocking Library presented at Ancient City Ruby 2013. This is not a direct transcript of the video, but the code presented is almost the same (with minimal changes).

In his talk, Andy showed us how we can build a Mocking library for Minitest with just basic knowledge of Ruby and I felt that it's actually a great way to learn Ruby! So I decided to document the talk in writing and share it up here on the blog so that we can all learn together.

Goal

We are going to implement a simple Mocking library for Minitest.

Given an object:

# Test double
object = Object.new

We should be able to stub a method on this object and it will return our stubbed value:

# Stub
allow(object).to receive(:full?).and_return(true)
object.full? # => true

We should be able to mock an object (mock will verify if removed was ever called, while stub does not do that check):

# Mock
item_id = 1234
assume(object).to receive(:remove).with(item_id)

Why don't we use expect(w).to receive(:remove).with(item_id) here, similar to RSpec? That's because Minitest has an #expect method, so let's avoid redefining it.

Design

We will have two main classes - StubTarget and ExpectationDefinition.

Remember our Goal? In order to be able to do this:

allow(w).to receive(:full?).and_return(true)

We'll break them up as follows using our two main classes:

allow(w).to      receive(:full?).and_return(true)
^^^^^^^^^^^      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
#<StubTarget>    #<ExpectationDefinition>

Enough Ruby that You Need to Know

Method Dispatch

How does Ruby find methods? It climbs up the ancestors chain! When you invoke to_s on object object, Ruby asks object...

Ruby: Hey, do you have to_s method?
object: Yup. I do.
Ruby: Awesome! Call it!
object: (invoking to_s)

Then it returns the result of object.to_s which is "#<Object:0x007fc0223b8280>":

> object = Object.new
=> #<Object:0x007fc0223b8280>

> object.to_s
=> "#<Object:0x007fc0223b8280>"

More on Method Dispatch

Ancestors Chain

If Ruby can't find the method you want to call, it will climb up the ancestors chain, till it finds a class that responds to the message, otherwise it eventually throws a NoMethodError exception.

object.class.ancestors
=> [Object, Kernel, BasicObject] # (searching from left to right)

Define a Method for a Specific Object

Ruby has a singleton class for every object and you can define a method in the singleton class.

The singleton class might be not visible in the ancestors chain above, but it's there.

Singleton Class

The "Singleton Class" is easily confused with the Singleton design pattern.

In fact, singleton class is an anonymous class attached to a specific object. Best illustrated with an example:

object = Object.new

def object.hello_world
  "Hello, World!"
end

object.hello_world # => "Hello, World!"

another_object = Object.new

object.hello_world # => NoMethodError (2)

In the example above, we are adding a hello_world method to the object. But the hello_world method wasn't added to the Object class (See (2) above).

As you can see, Ruby insert the hello_world method into object's singleton class!

More on Singleton Class

define_singleton_method

Another way to define a method for singleton class, is to use define_singleton_method(symbol, method_object).

The example above could be re-written as follows:

> object = Object.new
=> #<Object:0x007fc0223b8280>

> object.singleton_class
=> #<Class:#<Object:0x007fc0223b8280>>

> object.define_singleton_method(:hello_world) { "Hello, World!" }

The define_singleton_method accepts a method name and a Method object. Think of a Method object as similar to a proc or lambda.

That's enough Ruby that you need to know. Yup. That's all!

Building a Mock Object Library

Since this is a Mocking library, let's make it a gem!

Your Mocking Gem

Let's gemify our mocking library. You can name it using this pattern: yourname_mock. My name is Juanito and so I will call it juanito_mock, and we'll also use bundle gem command provided by Bundler to create a skeleton of our gem:

$ bundle gem juanito_mock && cd juanito_mock

Note that Bundler may prompt you to choose which test library you want to use, type minitest and hit <kbd>ENTER</kbd>.

Creating gem 'juanito_mock'...
MIT License enabled in config
Do you want to generate tests with your gem?
Type 'rspec' or 'minitest' to generate those test files now and in the future. rspec/minitest/(none):

If your generated skeleton gem has no tests or is generated with spec folder, edit ~/.bundle/config file, add this line (or modify):

BUNDLE_GEM__TEST: minitest

Remove the generated folder and repeat it from the top again.

The structure of the gem should look like this:

├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── bin
│   ├── console
│   └── setup
├── juanito_mock.gemspec
├── lib
│   ├── juanito_mock
│   │   └── version.rb
│   └── juanito_mock.rb
└── test
    ├── juanito_mock_test.rb
    └── test_helper.rb

Why Minitest?

Since we are implementing a RSpec-like mocking syntax, we don't want to use RSpec here so as to avoid confusions and conflicts with the original RSpec mocking library. Hence, we are going to use Minitest here to test our Mocking library.

By the way, the correct spelling of Minitest is Minitest, not MiniTest.

Renamed MiniTest to Minitest. Your pinkies will thank me.
Minitest 5.0.0 History

Setup Minitest

First lock Minitest to 5.8.0 in gemspec's development dependency in order to use it in development:

spec.add_development_dependency "minitest", "5.8.0"

Latest version of Minitest is 5.8.0 as of 16th Aug 2015.

Add these lines to test/test_helper.rb:

require "minitest/spec"
require "minitest/autorun"

Reorder and your test/test_helper.rb should look like this:

$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
require "minitest/spec" # simple and clean spec system
require "minitest/autorun" # easy and explicit way to run all your tests
require "juanito_mock"

Note on quotes of string. Just Use double-quoted strings

We use Minitest/Spec syntax to write our tests and require "minitest/autorun" to easily run all our tests.

Next, delete the generated tests in test/juanito_mock_test.rb and update it with DSL:

require "test_helper"

describe JuanitoMock do

end

Now if you run rake, you should have a working test suite:

$ rake
Run options: --seed 55155

# Running:


Finished in 0.000783s, 0.0000 runs/s, 0.0000 assertions/s.

0 runs, 0 assertions, 0 failures, 0 errors, 0 skips

Now let's write our first test!

Implementation of Stub

Create a test case by using it followed by a descriptive description string, and a block of code:

describe JuanitoMock do
  it "allows an object to receive a message and returns a value" do
    warehouse = Object.new

    allow(warehouse).to receive(:full?).and_return(true)

    warehouse.full?.must_equal true
  end
end

Let's walk through the code..

warehouse = Object.new

Firstly, we create a new instance of Object and assign it to a variable warehouse.

allow(warehouse).to receive(:full?).and_return(true)

Then, we create a stub that will receive the method full? and return the result true.

warehouse.full?.must_equal true

Finally, we verify our stub is working by using must_equal.

Sidenote: See the blank lines in our test? These blank lines are very important to distinguish different phases of the test.

More on Four Phase Test

How to Run Tests

First, let's take a look at Rakefile:

require "bundler/gem_tasks"
require "rake/testtask"

Rake::TestTask.new(:test) do |t|
  t.libs << "test"
  t.libs << "lib"
  t.test_files = FileList['test/**/*_test.rb']
end

task :default => :test

require "rake/testtask" included in a file with a rake task defined (rake test) that can run our tests easily via rake, see Rake::TestTask for more information.

You can see a full list of rake tasks available by typing rake -T in your terminal:

$ rake -T
rake build          # Build juanito_mock-0.1.0.gem into the pkg directory
rake install        # Build and install juanito_mock-0.1.0.gem into system ...
rake install:local  # Build and install juanito_mock-0.1.0.gem into system ...
rake release        # Create tag v0.1.0 and build and push juanito_mock-0.1...
rake test           # Run tests

The build, install, install:local, and release tasks are provided by Bundler. See bundler/bundler lib/bundler/gem_helper.rb

But you also see that rake test is available for use.

To make it even simple to run your tests, the Rakefile has this task :default => :test which basically maps the default rake task to running tests.

This means that you can just type rake instead of rake test to run all your tests.

Let's run it:

$ rake
Run options: --seed 49489

# Running:

E

Finished in 0.000963s, 1038.1963 runs/s, 0.0000 assertions/s.

  1) Error:
JuanitoMock#test_0001_allows an object to receive a message and returns a value:
NoMethodError: undefined method `allow' for #<#<Class:0x007fe04516bf70>:0x007fe045d001d8>
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:7:in `block (2 levels) in <top (required)>'

1 runs, 0 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

Yay! Our first failing test, read the error carefully to find out what to do next:

undefined method `allow' for #<#<Class:0x007fe04516bf70>:0x007fe045d001d8>
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:7:in `block (2 levels) in <top (required)>'

It even tells you which line to fix the code:

test/juanito_mock_test.rb:7

:7 means line 7 from the file test/juanito_mock_test.rb.

Add DSL to Minitest

Let's proceed to fix the failing test.

From Minitest README, we know every test in Minitest is a subclass of Minitest::Test:

To add method allow to Minitest::Test, all we have to do is to create a module TestExtensions and include it in the Minitest::Test class:

require "juanito_mock/version"

module JuanitoMock
  module TestExtensions
    def allow
    end
  end
end

class Minitest::Test
  include JuanitoMock::TestExtensions
end

Let's run our test:

$ rake
Run options: --seed 41300

# Running:

E

Finished in 0.001002s, 997.6067 runs/s, 0.0000 assertions/s.

  1) Error:
JuanitoMock#test_0001_allows an object to receive a message and returns a value:
ArgumentError: wrong number of arguments (1 for 0)
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:5:in `allow'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:9:in `block (2 levels) in <top (required)>'

1 runs, 0 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

Nice! Now we get a different error:

ArgumentError: wrong number of arguments (1 for 0)
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:5:in `allow'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:9:in `block (2 levels) in <top (required)>'

The error occurs because we are calling allow like so allow(warehouse) in our code, which means we are passing in an argument warehouse which our allow method doesn't accept yet.

allow(warehouse).to receive(:full?).and_return(true)

Let's fix this by modifying our allow method to accept an argument obj. Then, we'll construct an instance of StubTarget with the argument, as described in our design:

require "juanito_mock/version"

module JuanitoMock
  module TestExtensions
    def allow(obj)
      StubTarget.new(obj)
    end
  end
end

class Minitest::Test
  include JuanitoMock::TestExtensions
end

Now run the test again:

$ rake
Run options: --seed 7548

# Running:

E

Finished in 0.000915s, 1092.3434 runs/s, 0.0000 assertions/s.

  1) Error:
JuanitoMock#test_0001_allows an object to receive a message and returns a value:
NameError: uninitialized constant JuanitoMock::TestExtensions::StubTarget
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:6:in `allow'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:9:in `block (2 levels) in <top (required)>'

1 runs, 0 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

Another error this time:

NameError: uninitialized constant JuanitoMock::TestExtensions::StubTarget
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:6:in `allow'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:9:in `block (2 levels) in <top (required)>'

Ruby is now complaining that it can't find the constant JuanitoMock::TestExtensions::StubTarget. Of course! That's because we haven't define StubTarget class yet, so let's define it:

module JuanitoMock
  class StubTarget
    def initialize(obj)
      @obj = obj
    end
  end

  module TestExtensions
    def allow(obj)
      StubTarget.new(obj)
    end
  end
end

class Minitest::Test
  include JuanitoMock::TestExtensions
end

For a start, we will just save the obj in an instance variable.

Now run the test again:

$ rake
Run options: --seed 25153

# Running:

E

Finished in 0.001059s, 944.4913 runs/s, 0.0000 assertions/s.

  1) Error:
JuanitoMock#test_0001_allows an object to receive a message and returns a value:
NoMethodError: undefined method `receive' for #<#<Class:0x007fe080b5d430>:0x007fe081057058>
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:9:in `block (2 levels) in <top (required)>'

1 runs, 0 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

What? Another error. This doesn't seem like it's ending soon. But you should actually rejoice, because we now have a different error, and that means we are progressing!

NoMethodError: undefined method `receive' for #<#<Class:0x007fe080b5d430>:0x007fe081057058>
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:9:in `block (2 levels) in <top (required)>'

This time it's ranting about another missing method receive. Hmm what about to? Why didn't it complain about a missing method to?

That's because Ruby always tries to evaluate the right-hand side first, and so it's going to process receive first before it gets to to. Dont' worry, you'll see an error for to later.

Let's define a receive method in TestExtensions module which accepts a message:

require "juanito_mock/version"

module JuanitoMock
  class StubTarget
    ...
  end

  module TestExtensions
    def allow(obj)
      ...
    end

    def receive(message)
      ExpectationDefinition.new(message)
    end
  end
end

As described in design section, receive will return a ExpectationDefinition instance.

Now run the test again:

$ rake
Run options: --seed 27383

# Running:

E

Finished in 0.001145s, 873.0986 runs/s, 0.0000 assertions/s.

  1) Error:
JuanitoMock#test_0001_allows an object to receive a message and returns a value:
NameError: uninitialized constant JuanitoMock::TestExtensions::ExpectationDefinition
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:16:in `receive'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:9:in `block (2 levels) in <top (required)>'

1 runs, 0 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

You probably already expected it and now Ruby complains that it cannot find ExpectationDefinition:

NameError: uninitialized constant JuanitoMock::TestExtensions::ExpectationDefinition
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:16:in `receive'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:9:in `block (2 levels) in <top (required)>'

Let's go ahead and define it, keeping ExpectationDefinition simple, such that it only accepts an argument and stores it in an instance variable.

module JuanitoMock
  class StubTarget
    ...
  end

  class ExpectationDefinition
    def initialize(message)
      @message = message
    end
  end

  module TestExtensions
    ...
  end
end

Now run the test again:

$ rake
Run options: --seed 47423

# Running:

E

Finished in 0.001005s, 995.4657 runs/s, 0.0000 assertions/s.

  1) Error:
JuanitoMock#test_0001_allows an object to receive a message and returns a value:
NoMethodError: undefined method `and_return' for #<JuanitoMock::ExpectationDefinition:0x007fe072f6a798>
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:9:in `block (2 levels) in <top (required)>'

1 runs, 0 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

Now Ruby cannot find the and_return method
(poor Ruby, thanks for doing so much work for us 😢):

NoMethodError: undefined method `and_return' for #<JuanitoMock::ExpectationDefinition:0x007fe072f6a798>
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:9:in `block (2 levels) in <top (required)>'

Looking at the error, it's actually telling us that it cannot find the and_return method on ExpectationDefinition, so let's define it there:

require "juanito_mock/version"

module JuanitoMock
  class StubTarget
    ...
  end

  class ExpectationDefinition
    def initialize(message)
      @message = message
    end

    def and_return(return_value)
      @return_value = return_value
      self
    end
  end

  module TestExtensions
    ...
  end
end

This new method and_return is interesting and contains the secret to enabling method chaining.

Do you know what it is?

Yes. The method is returning self!

def and_return(return_value)
  @return_value = return_value
  self
end

That's the magic to building a chaining interface for your objects! All you have to do is to build up objects and return self!

Now let's run our test again:

$ rake
Run options: --seed 11338

# Running:

E

Finished in 0.001084s, 922.9213 runs/s, 0.0000 assertions/s.

  1) Error:
JuanitoMock#test_0001_allows an object to receive a message and returns a value:
NoMethodError: undefined method `to' for #<JuanitoMock::StubTarget:0x007ff48a516590>
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:9:in `block (2 levels) in <top (required)>'

1 runs, 0 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

Yes! Now we see the error for undefined method to on StubTarget class, and so let's define the to method on StubTarget class according to our design:

allow(warehouse).to receive(:full?).and_return(true)
^^^^^^^^^^^^^^^^    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    StubTarget            ExpectationDefinition

where the to method accepts an ExpectationDefinition object as argument.

module JuanitoMock
  class StubTarget
    def initialize(obj)
      @obj = obj
    end

    def to(definition)
    end
  end

  class ExpectationDefinition
    ...
  end

  module TestExtensions
    ...
  end
end

Now let's run our test again:

$ rake
Run options: --seed 31564

# Running:

E

Finished in 0.001092s, 915.9027 runs/s, 0.0000 assertions/s.

  1) Error:
JuanitoMock#test_0001_allows an object to receive a message and returns a value:
NoMethodError: undefined method `full?' for #<Object:0x007f8fcc47dc28>
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:11:in `block (2 levels) in <top (required)>'

1 runs, 0 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

Reading our next failure, the error guides us to define a full? method on the object:

NoMethodError: undefined method `full?' for #<Object:0x007f8fcc47dc28>
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:11:in `block (2 levels) in <top (required)>'

Let's use the aforementioned define_singleton_method magic to define the full? method, and which returns the expected value of true as specified in our test:

module JuanitoMock
  class StubTarget
    ...

    def to(definition)
      @obj.define_singleton_method definition.message do
        definition.return_value
      end
    end
  end

  class ExpectationDefinition
    ...
  end

  module TestExtensions
    ...
  end
end

Now run the tests again:

$ rake
Run options: --seed 14082

# Running:

E

Finished in 0.001091s, 916.4954 runs/s, 0.0000 assertions/s.

  1) Error:
JuanitoMock#test_0001_allows an object to receive a message and returns a value:
NoMethodError: undefined method `message' for #<JuanitoMock::ExpectationDefinition:0x007ff31d8ae220>
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:10:in `to'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:9:in `block (2 levels) in <top (required)>'

1 runs, 0 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

This time Ruby cannot find message on ExpectationDefinition:

NoMethodError: undefined method `message' for #<JuanitoMock::ExpectationDefinition:0x007ff31d8ae220>
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:10:in `to'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:9:in `block (2 levels) in <top (required)>'

Keeping it simple, we expose message and return_value in ExpectationDefinition class with attr_reader:

module JuanitoMock
  ...

  class ExpectationDefinition
    attr_reader :message, :return_value

    def initialize(message)
      @message = message
    end

    ...
  end

  ...
end

Now run this test again:

$ rake
Run options: --seed 46498

# Running:

.

Finished in 0.000975s, 1025.2078 runs/s, 1025.2078 assertions/s.

1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

Our first passing test!

WOO-HOO!

allow(warehouse).to receive(:full?).and_return(true)

This line in the test is actually passing! Just with 42 lines of code in a relatively short amount of time!

Let's take a step back to see what we have done so far. Basically, we started to build our Mocking library by writing a test - a failing test. Then we write some code to error that was thrown, and some more code to fix the next error that was thrown and so on and so forth. And finally, through our persistence, we got the test to pass!

This practice of writing software is what we call Test Driven Development (TDD), where we go from red (failing test), to green (passing test) and moving on to refactor. This is a practice that a lot of software engineers embrace, and it's one which we have found immense benefits when doing it consistently.

The Edge of Mocking

edge-of-tomorrow-13

Are we done already? Not quite!

In our current code, we defined the full?method on the object when the test starts, but we didn't do anything to reset our change after the test finishes, and that's actually not so good, because it might affect other tests. So, we should reset the state and unset the full? method that we have "stubbed".

Let's write another test for this:

  it "removes stubbed method after tests finished" do
    warehouse = Object.new

    allow(warehouse).to receive(:full?).and_return(true)

    JuanitoMock.reset

    assert_raises(NoMethodError) { warehouse.full? }
  end

I intentionally prefix each line with two spaces to make it copy-paste friendly, but I strongly encourage you to type on your own.

In the above test, we invoke JuanitoMock.reset to clear/undo all changes to the code, and we verify this by using assert_raises where we test that an exception is raised
when full? is invoked.

At this point, this is how your test file should look like:

require "test_helper"

describe JuanitoMock do
  it "allows an object to receive a message and returns a value" do
    warehouse = Object.new

    allow(warehouse).to receive(:full?).and_return(true)

    warehouse.full?.must_equal true
  end

  it "removes stubbed method after tests finished" do
    warehouse = Object.new

    allow(warehouse).to receive(:full?).and_return(true)

    JuanitoMock.reset

    assert_raises(NoMethodError) { warehouse.full? }
  end
end

Once again, we run the test to find out what to do next:

$ rake
Run options: --seed 30441

# Running:

.E

Finished in 0.001152s, 1736.7443 runs/s, 868.3721 assertions/s.

  1) Error:
JuanitoMock#test_0002_removes stubbed method after tests finished:
NoMethodError: undefined method `reset' for JuanitoMock:Module
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:17:in `block (2 levels) in <top (required)>'

2 runs, 1 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

Oops. Did you expect that - undefined method reset for JuanitoMock:Module?

Let's write this method! Edit lib/juanito_mock.rb and add this module-level method:

module JuanitoMock
  ...

  module TestExtensions
    ...
  end

  def self.reset
  end
end

What do we do next? What should we write in the method? Run the test and let that help us!

$ rake
Run options: --seed 22585

# Running:

.F

Finished in 0.001207s, 1657.1930 runs/s, 1657.1930 assertions/s.

  1) Failure:
JuanitoMock#test_0002_removes stubbed method after tests finished [/Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:19]:
NoMethodError expected but nothing was raised.

2 runs, 2 assertions, 1 failures, 0 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

NoMethodError expected but nothing was raised.. Yup. That's what I expected, too.

How can we undefine the method full? that we have stubbed on the object? As we had defined the method full? in the object's singleton class, there is actually no reference to it and so we don't know how to undefine it, at least for now...

Let's park that for now, and do something else instead (which would help us later).

Let's improve the code!

We are going to make some changes to our code without losing the any of our current functionality.

Let's start by wrapping the define method step into a class, a delegate class and name it: Stubber. Put Stubber below StubTarget and above the ExpectationDefinition:

module JuanitoMock
  class StubTarget
    ...
  end

  class Stubber
    def initialize(obj)
      @obj = obj
    end

    def stub(definition)
    end
  end

  class ExpectationDefinition
    ...
  end
end

Then, move the implementation of StubTarget#to to Stubber#stub:

module JuanitoMock
  class StubTarget
    ...
  end

  class Stubber
    def initialize(obj)
      @obj = obj
    end

    def stub(definition)
      @obj.define_singleton_method definition.message do
        definition.return_value
      end
    end
  end

  class ExpectationDefinition
    ...
  end
end

In StubTarget#to, we delegate the job to Stubber#stub:

module JuanitoMock
  class StubTarget
    ...

    def to(definition)
      Stubber.new(@obj).stub(definition)
    end
  end

  ...
end

Nice refactoring! This is an essential step in the TDD practice, and what we just did was basically to improve our code without modifying the current feature set of our code. We can verify this by running our tests which would prove that the first test is green, while the second test is still red:

$ rake
Run options: --seed 23169

# Running:

.F

Finished in 0.001183s, 1691.2146 runs/s, 1691.2146 assertions/s.

  1) Failure:
JuanitoMock#test_0002_removes stubbed method after tests finished [/Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:19]:
NoMethodError expected but nothing was raised.

2 runs, 2 assertions, 1 failures, 0 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

Let's get back to fixing the error.

Let's store what we stubbed in an array called @definitions, in Stubber#stub:

module JuanitoMock
  ...

  class Stubber
    def initialize(obj)
      @obj = obj
      @definitions = []
    end

    def stub(definition)
      @definitions << definition

      @obj.define_singleton_method definition.message do
        definition.return_value
      end
    end
  end

  ...
end

However, having the @definitions array is not enough because the Stubber instance in:

    def to(definition)
      Stubber.new(@obj).stub(definition)
    end

immediately goes out of scope and gets garbage collected, and so we still do not have a list of all methods that were stubbed.

Hence we need to be able to save the Stubber instance(s) by using a Stubber.for_object class-level method:

module JuanitoMock
  class StubTarget
    def initialize(obj)
      @obj = obj
    end

    def to(definition)
      Stubber.for_object(@obj).stub(definition)
    end
  end

  ...
end

Now run the test again:

$ rake
Run options: --seed 37100

# Running:

EE

Finished in 0.001317s, 1518.4864 runs/s, 0.0000 assertions/s.

  1) Error:
JuanitoMock#test_0002_removes stubbed method after tests finished:
NoMethodError: undefined method `for_object' for JuanitoMock::Stubber:Class
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:10:in `to'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:15:in `block (2 levels) in <top (required)>'


  2) Error:
JuanitoMock#test_0001_allows an object to receive a message and returns a value:
NoMethodError: undefined method `for_object' for JuanitoMock::Stubber:Class
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:10:in `to'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:7:in `block (2 levels) in <top (required)>'

2 runs, 0 assertions, 0 failures, 2 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

The next error to fix is to define for_object on Stubber class:

  NoMethodError: undefined method `for_object' for JuanitoMock::Stubber:Class
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:10:in `to'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:7:in `block (2 levels)

This Stubber.for_object method is a custom initializer for the Stubber class that will not only create Stubber instances, but also store them in a lazily-initialized hash, with its object_id as key:

module JuanitoMock
  class Stubber
    def self.stubbers
      @stubbers ||= {}
    end

    def self.for_object(obj)
      stubbers[obj.__id__] ||= Stubber.new(obj)
    end

    ...
  end
end

But are we making progress for JuanitoMock.reset? Hmm.. Let's run the tests first.

$ rake
Run options: --seed 4701

# Running:

.F

Finished in 0.001117s, 1789.8854 runs/s, 1789.8854 assertions/s.

  1) Failure:
JuanitoMock#test_0002_removes stubbed method after tests finished [/Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:19]:
NoMethodError expected but nothing was raised.

2 runs, 2 assertions, 1 failures, 0 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

The failure is still the same as before, but we are actually making a progress.

Given that all the stubbed methods are now stored in the Stubber class, it would be great if we have one single method in Stubber that can help us. Thinking along that line of thought, let's have a Stubber.reset method that does exactly that, and then it would be trivial for JuanitoMock.reset to invoke it!

Let's try to implement the logic for Stubber.reset that we wish we have.

stubbers currently is a hash that looks like this:

{
  70173643198180 => #<Stubber instance>
}

It is a one-to-one object id mapping to a Stubber instance. We would first want each instance to unstub the method that we stub earlier. The intent is still similar, so each instance should have its own reset method that we can call. Also, the reset method should empty the hash after we are done with it.
Cool! Ruby has a clear method that we can use.

module JuanitoMock
  ...

  class Stubber
    ...

    def self.for_object(obj)
      ...
    end

    def self.reset
      stubbers.each_value(&:reset)
      stubbers.clear
    end

    ...
  end

  ...

  module TestExtensions
    ...
  end

  def self.reset
    Stubber.reset
  end
end

Run the tests again:

$ rake
Run options: --seed 11282

# Running:

.E

Finished in 0.001142s, 1751.6509 runs/s, 875.8255 assertions/s.

  1) Error:
JuanitoMock#test_0002_removes stubbed method after tests finished:
NoMethodError: undefined method `reset' for #<JuanitoMock::Stubber:0x007f9a85c7bf38>
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:24:in `each_value'
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:24:in `reset'
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:66:in `reset'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:17:in `block (2 levels) in <top (required)>'

2 runs, 1 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

Now we have an undefined method reset for #<JuanitoMock::Stubber:0x007f9a85c7bf38> - a Stubber instance:

JuanitoMock#test_0002_removes stubbed method after tests finished:
NoMethodError: undefined method `reset' for #<JuanitoMock::Stubber:0x007f9a85c7bf38>

Let's implement Stubber#reset. In Stubber#reset, what we need to do is to undefine/unstub the method we have defined/stubbed earlier.

In Ruby, we can use remove_method with some class_eval craziness to achieve this:

module JuanitoMock
  ...

  class Stubber
    ...

    def stub(definition)
      ...
    end

    def reset
      @definitions.each do |definition|
        @obj.singleton_class.class_eval do
          remove_method(definition.message) if method_defined?(definition.message)
        end
      end
    end
  end

  ...
end

We avoid the NoMethodError exception by checking method_defined? on definition.message.

Now if you are brave enough to run the tests:

$ rake
Run options: --seed 55448

# Running:

..

Finished in 0.001233s, 1622.4943 runs/s, 1622.4943 assertions/s.

2 runs, 2 assertions, 0 failures, 0 errors, 0 skips

You shall see all our tests passed! All green! Yay!!!

gatsby-screenshot2-e1397167545470

All tests passed, old sport! Can we live happily ever after now? Hmm...

Blindly Removing Methods

Till now, we have covered the cases of stubbing and unstubbing. But there's actually a third case to consider!

What if, at the very beginning, there was already a full? method defined? We would have "killed" or "replaced" the original method unknowingly.

Let's write another test to describe this case:

  it "preserves methods that originally existed" do
    warehouse = Object.new
    def warehouse.full?; false; end # defining methods on Ruby singleton class

    allow(warehouse).to receive(:full?).and_return(true)

    JuanitoMock.reset

    warehouse.full?.must_equal false
  end

Run the tests:

$ rake
Run options: --seed 4474

# Running:

.E.

Finished in 0.001266s, 2369.2790 runs/s, 1579.5193 assertions/s.

  1) Error:
JuanitoMock#test_0003_preserves methods that are originally existed:
NoMethodError: undefined method `full?' for #<Object:0x007fcdcd4d3b70>
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:29:in `block (2 levels) in <top (required)>'

3 runs, 2 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

We have a failure on our new test!

What happened was that our stub allow(warehouse).to receive(:full?).and_return(true) replaced the original method, and when we called JuanitoMock.reset, it only removed the stub but didn't bring back the original implementation for full?.

Hence the NoMethodError exception is expected, because the method's basically gone!

This is not ok. Let's fix that. But first let's take a look at Stubber#stub method:

class Stubber
  ...

  def stub(definition)
    @definitions << definition

    # preserve original method if already exists

    @obj.define_singleton_method definition.message do
      definition.return_value
    end
  end

  ...
end

In our current implementation of Stubber#stub, we didn't check if the object already has the method or not and we just simply (re)defined the singleton method.

We should preserve the original method if it already exists like so:

  class Stubber
    ...

    def stub(definition)
      @definitions << definition

      if @obj.singleton_class.method_defined?(definition.message)
        @preserved_methods <<
          @obj.singleton_class.instance_method(definition.message)
      end

      @obj.define_singleton_method definition.message do
        definition.return_value
      end
    end

    ...
  end

Let's walk through what we just did:

@obj.singleton_class.instance_method(definition.message)

The magic comes from the use of Module#instance_method which will return a method object of given name from the singleton class.

Think of this method object as a proc or lambda which we then we store in a @preserved_methods array:

  class Stubber
    ...

    def initialize(obj)
      @obj = obj
      @definitions = []
      @preserved_methods = []
    end

    ...
  end

Preserving the orignal method is only one part of the solution.

When we do a Stubber#reset, we actually want to reinstate and redefine these saved preserved methods:

  class Stubber
    ...

    def reset
      ...

      @preserved_methods.reverse_each do |method|
        @obj.define_singleton_method(method.name, method)
      end
    end
  end

We use reverse_each here because we need to preserve the original order of the methods. You can write a test here too to see the importance of using reverse_each but we'll leave it as an exercise!

P.S. Did you know reverse_each is more efficient than reverse.each?

In Stubber#stub, we used obj.define_singleton_method with a block, but it also pairs really well with method objects that we are dealing with in the @preserved_methods array.

Every method object Method#instance_method has a Method#name method that returns the name of the method. We can simply redefine the method by calling define_singleton_method with the method name and the method object itself.

Run the tests again and we should have three passing tests:

$ rake
Run options: --seed 63328

# Running:

...

Finished in 0.001223s, 2453.9275 runs/s, 2453.9275 assertions/s.

3 runs, 3 assertions, 0 failures, 0 errors, 0 skips

This also means we have successfully restored the original methods after the tests!

Congrats on getting so far, but we are not quite done. By now we have only implemented stub (and unstub), and next we are going to implement mock - which is an expectation that a message will be received.

To Mock a Mockingbird

mimus_polyglottos1

Let's start with a new failing test as usual:

  it "expects that a message will be received" do
    warehouse = Object.new

    assume(warehouse).to receive(:empty)

    # warehouse.empty not called!

    assert_raises(JuanitoMock::ExpectationNotSatisfied) do
      JuanitoMock.reset
    end
  end

In our test, assume(warehouse).to receive(:empty) expects that warehouse.empty will be invoked. However we are not actually going to call the empty method and so, we assert that a custom exception JuanitoMock::ExpectationNotSatisfied will be raised when we call JuanitoMock.reset which loops and verfies each expectation.

Let's run the test:

$ rake
Run options: --seed 30442

# Running:

..E.

Finished in 0.001308s, 3058.4267 runs/s, 2293.8200 assertions/s.

  1) Error:
JuanitoMock#test_0004_expects that a message will be received:
NoMethodError: undefined method `assume' for #<#<Class:0x007facec071a20>:0x007facecbea5a0>
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:35:in `block (2 levels) in <top (required)>'

4 runs, 3 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

The first thing we see is:

NoMethodError: undefined method `assume' for #<#<Class:0x007facec071a20>:0x007facecbea5a0>
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:35:in `block (2 levels) in <top (required)>'

We have not define assume in our TestExtensions module, hence the error message. Let's do that! (TestExtensions should be at the bottom of lib/juanito_mock.rb):

module JuanitoMock
  ...

  module TestExtensions
    def allow(obj)
      ...
    end

    def assume(obj)
    end

    def receive(message)
      ...
    end
  end

  def self.reset
    ...
  end
end

Instead of an instance of StubTarget, let's return an instance of ExpectationTarget:

module JuanitoMock
  ...

  module TestExtensions
    ...

    def assume(obj)
      ExpectationTarget.new(obj)
    end

    ...
  end

  def self.reset
    ...
  end
end

Now if you run the tests, it will complain that ExpectationTarget is undefined:

$ rake
Run options: --seed 58985

# Running:

...E

Finished in 0.001235s, 3237.5687 runs/s, 2428.1766 assertions/s.

  1) Error:
JuanitoMock#test_0004_expects that a message will be received:
NameError: uninitialized constant JuanitoMock::TestExtensions::ExpectationTarget
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:79:in `assume'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:35:in `block (2 levels) in <top (required)>'

4 runs, 3 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

We use ExpectationTarget in assume because it's a target of a mock expectation (vs. a stub). However, ExpectationTarget is actully very similar to a StubTarget (a specialized form of StubTarget), in that both stubs the original method implementation of an object, but ExpectationTarget does a little something extra by checking that the message has been called.

allow(object).to receive(:message)
assume(object).to receive(:message)

Hence we can make ExpectationTarget a subclass of StubTarget, and let to method in ExpectationTarget inherit the implementation of to method in StubTarget by using super. Then we also store the definition object to a not-yet-exist JuanitoMock.expectations array, so that we can use that to perform our expectation checks later:

module JuanitoMock
  class StubTarget
    ...
  end

  class ExpectationTarget < StubTarget
    def to(definition)
      super
      JuanitoMock.expectations << definition
    end
  end
end

Now run the tests again:

$ rake
Run options: --seed 53163

# Running:

...E

Finished in 0.001286s, 3109.3585 runs/s, 2332.0189 assertions/s.

  1) Error:
JuanitoMock#test_0004_expects that a message will be received:
NoMethodError: undefined method `expectations' for JuanitoMock:Module
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:17:in `to'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:35:in `block (2 levels) in <top (required)>'

4 runs, 3 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

You know the drill. Let's initialize the expectations array, lazily:

module JuanitoMock
  ...

  module TestExtensions
    ...
  end

  def self.reset
    Stubber.reset
  end

  def self.expectations
    @expectations ||= []
  end
end

Run the tests once more and make a little bit more progress:

$ rake
Run options: --seed 51829

# Running:

.E..

Finished in 0.001005s, 3980.0243 runs/s, 2985.0182 assertions/s.

  1) Error:
JuanitoMock#test_0004_expects that a message will be received:
NameError: uninitialized constant JuanitoMock::ExpectationNotSatisfied
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:39:in `block (2 levels) in <top (required)>'

4 runs, 3 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

Where's the class JuanitoMock::ExpectationNotSatisfied? Oops we don't have that yet, so let's fix it:

module JuanitoMock
  ExpectationNotSatisfied = Class.new(StandardError)

  class StubTarget
    ...
  end

  ...
end

Define a simple exception class and run the tests again. You'll see that JuanitoMock::ExpectationNotSatisfied expected but nothing was raised.:

$ rake
Run options: --seed 27046

# Running:

...F

Finished in 0.001435s, 2788.1462 runs/s, 2788.1462 assertions/s.

  1) Failure:
JuanitoMock#test_0004_expects that a message will be received [/Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:39]:
JuanitoMock::ExpectationNotSatisfied expected but nothing was raised.

4 runs, 4 assertions, 1 failures, 0 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

That's expected. We merely created the ExpectationTarget and ExpectationNotSatisfied classes, and we have not added anything new to the Stubber.reset method, so it's right that the new test is failing.

What should Stubber.reset do that would make our test pass? Hmm.. Stubber.reset should be checking that all of our expectations are verified, and would raise an error if any of the expectations failed. Why don't we add a verify method to each Stubber instance that would do the checking?

module JuanitoMock
  ...

  module TestExtensions
    ...
  end

  def self.reset
    expectations.each(&:verify)
    Stubber.reset
  end

  def self.expectations
    @expectations ||= []
  end
end

This works, but if an exceptation is raised when verify fails, then Stubber.reset would not actually be executed because the exception would have broke the control flow.

We want to make sure that Stubber.reset is called even if any expectation raised an exception, and we also want to clear @expectations too so that weird things won't happen. Ruby's ensure is here to help:

module JuanitoMock
  ...

  module TestExtensions
    ...
  end

  def self.reset
    expectations.each(&:verify)
  ensure
    expectations.clear
    Stubber.reset
  end

  ...
end

Run the tests again:

$ rake
Run options: --seed 12211

# Running:

...F

Finished in 0.001460s, 2738.9588 runs/s, 2738.9588 assertions/s.

  1) Failure:
JuanitoMock#test_0004_expects that a message will be received [/Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:39]:
[JuanitoMock::ExpectationNotSatisfied] exception expected, not
Class: <NoMethodError>
Message: <"undefined method `verify' for #<JuanitoMock::ExpectationDefinition:0x007f89bb4b9640>">
---Backtrace---
/Users/Juan/null/juanito_mock/lib/juanito_mock.rb:97:in `each'
/Users/Juan/null/juanito_mock/lib/juanito_mock.rb:97:in `reset'
/Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:40:in `block (3 levels) in <top (required)>'
---------------

4 runs, 4 assertions, 1 failures, 0 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

Getting there! We have an undefined method verify on ExpectationDefinition. Let's do the simplest thing to make the test pass! We'll define the verify method and just raise ExpectationNotSatisfied:

module JuanitoMock
  ...

  class ExpectationDefinition
    ...

    def and_return(return_value)
      @return_value = return_value
      self
    end

    def verify
      raise ExpectationNotSatisfied
    end
  end

  ...
end

Run the tests! All green!

$ rake
Run options: --seed 22996

# Running:

....

Finished in 0.001272s, 3143.7915 runs/s, 3143.7915 assertions/s.

4 runs, 4 assertions, 0 failures, 0 errors, 0 skips

But this is clearly not right even though we have all passing tests. We have a gap in our testing and we'll expose that gap by writing a new test:

  it "does not raise an error if expectations are satisfied" do
    warehouse = Object.new

    assume(warehouse).to receive(:empty)

    warehouse.empty

    JuanitoMock.reset # assert nothing raised!
  end

Now run the tests again:

$ rake
Run options: --seed 46019

# Running:

E....

Finished in 0.001292s, 3870.3166 runs/s, 3096.2532 assertions/s.

  1) Error:
JuanitoMock#test_0005_does not raise an error if expectations are satisfied:
JuanitoMock::ExpectationNotSatisfied: JuanitoMock::ExpectationNotSatisfied
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:82:in `verify'
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:101:in `each'
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:101:in `reset'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:51:in `block (2 levels) in <top (required)>'

5 runs, 4 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

The new test is failing now because verify always raises an exception! That's our cue to implement the actual logic for the verify method which checks if a method has been invoked.

Again, a simple way to solve this would be to use an invocation count as verification, like so:

module JuanitoMock
  ...

  class ExpectationDefinition
    def initialize(message)
      @message = message
      @invocation_count = 0
    end

    ...

    def verify
      if @invocation_count != 1
        raise ExpectationNotSatisfied
      end
    end
  end

  ...
end

But we don't really have a way to increment invocation count. Maybe...

Let's look at the following in Stubber#stub:

@obj.define_singleton_method definition.message do
  definition.return_value
end

When we define the singleton method, we are just simply returning the value via definition.return_value. Instead, let's modify it to look like:

@obj.define_singleton_method definition.message do
  definition.call
end

Invoking a call method is a standard practice if you want an object to act like a callable piece of code, like a proc or lambda (which also has the call method).

Let's run the tests:

$ rake
Run options: --seed 56524

# Running:

.E..E

Finished in 0.001311s, 3815.1804 runs/s, 2289.1082 assertions/s.

  1) Error:
JuanitoMock#test_0001_allows an object to receive a message and returns a value:
NoMethodError: undefined method `call' for #<JuanitoMock::ExpectationDefinition:0x007fa065d1a030>
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:52:in `block in stub'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:9:in `block (2 levels) in <top (required)>'


  2) Error:
JuanitoMock#test_0005_does not raise an error if expectations are satisfied:
NoMethodError: undefined method `call' for #<JuanitoMock::ExpectationDefinition:0x007fa065d18b40>
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:52:in `block in stub'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:49:in `block (2 levels) in <top (required)>'

5 runs, 3 assertions, 0 failures, 2 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

Let's add the method call for ExpectationDefinition:

module JuanitoMock
  ...

  class ExpectationDefinition
    ...

    def call
      @invocation_count += 1
      @return_value
    end

    def verify
      ...
    end
  end

  ...
end

The call method will still return the return_value (as was happening earlier with definition.return_value), but at the same time, it also increases the @invocation_count.

Now run the tests again, and we would be all green again!

$ rake
Run options: --seed 47647

# Running:

.....

Finished in 0.001300s, 3846.9144 runs/s, 3077.5315 assertions/s.

5 runs, 4 assertions, 0 failures, 0 errors, 0 skips

Great! We now have basic stub and mock functionality for JuanitoMock. But we don't have yet the ability to pass (and expect) arguments to stubs.

Let's write a test for that:

  it "allows object to receive messages with arguments" do
    warehouse = Object.new

    allow(warehouse).to receive(:include?).with(1234).and_return(true)
    allow(warehouse).to receive(:include?).with(9876).and_return(false)

    warehouse.include?(1234).must_equal true
    warehouse.include?(9876).must_equal false
  end

Now run the tests to see what should we do next:

$ rake
Run options: --seed 17857

# Running:

E.....

Finished in 0.001458s, 4116.0535 runs/s, 2744.0357 assertions/s.

  1) Error:
JuanitoMock#test_0006_allows object to receive messages with arguments:
NoMethodError: undefined method `with' for #<JuanitoMock::ExpectationDefinition:0x007fb1f2d010f0>
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:57:in `block (2 levels) in <top (required)>'

6 runs, 4 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

We don't have the with on ExpectationDefinition. Let's do it:

module JuanitoMock
  ...

  class ExpectationDefinition
    def and_return
      ...
    end

    def with(*arguments)
      @arguments = arguments
      self
    end

    def call
      ...
    end

    ...
  end

  ...
end

The with method will accept an array of arguments, made possible using the splat operator (*), and we also return self to make it chainable.

Run the tests again:

$ rake
Run options: --seed 16039

# Running:

...E..

Finished in 0.001207s, 4969.8536 runs/s, 3313.2358 assertions/s.

  1) Error:
JuanitoMock#test_0006_allows object to receive messages with arguments:
ArgumentError: wrong number of arguments (1 for 0)
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:51:in `block in stub'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:60:in `block (2 levels) in <top (required)>'

6 runs, 4 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

Now we see ArgumentError: wrong number of arguments (1 for 0).

Let's decrypt this error message.

What it's saying is that you passed in 1 argument, but the method defined only requires 0 arguments.

Luckily there is also a line number telling us where things went wrong:

lib/juanito_mock.rb:51:in `block in stub'

Line 51 or Stubber#stub should be:

@obj.define_singleton_method definition.message do
  definition.call
end

Let's allow the define_singleton_method block to accept splat arguments as well:

@obj.define_singleton_method definition.message do |*arguments|
  definition.call
end

Run the tests again:

$ rake
Run options: --seed 3190

# Running:

..F...

Finished in 0.001697s, 3536.4931 runs/s, 2947.0775 assertions/s.

  1) Failure:
JuanitoMock#test_0006_allows object to receive messages with arguments [/Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:60]:
Expected: true
  Actual: false

6 runs, 5 assertions, 1 failures, 0 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

And we now have a failure on our test:

  warehouse.include?(1234).must_equal true
  warehouse.include?(9876).must_equal false
end

Let's look at the test again:

it "allows object to receive messages with arguments" do
  warehouse = Object.new

  allow(warehouse).to receive(:include?).with(1234).and_return(true)
  allow(warehouse).to receive(:include?).with(9876).and_return(false)

  warehouse.include?(1234).must_equal true
  warehouse.include?(9876).must_equal false
end

warehouse.include?(1234) is returning false (and failing the test). That's because we have yet to do any matching on the stub argument and so the last stub

allow(warehouse).to receive(:include?).with(9876).and_return(false)

is the one that's being returned, no matter what arguments are used.

Why is the last stub returned? Remember when we defined the singleton method:

@obj.define_singleton_method definition.message do |*arguments|
  definition.call
end

We only invoked a definition via definition.call, but we didn't actually invoke the right definition.

Similar to our reset method, we should (reverse) search and find the definition that matches the method name and arguments:

@obj.define_singleton_method definition.message do |*arguments|
  @definitions
    .reverse
    .find { |definition| definition.matches(definition.message, *arguments) }
    .call
end

Run the tests again:

$ rake
Run options: --seed 61311

# Running:

E.EEE.

Finished in 0.001315s, 4563.6538 runs/s, 1521.2179 assertions/s.

  1) Error:
JuanitoMock#test_0001_allows an object to receive a message and returns a value:
NoMethodError: undefined method `reverse' for nil:NilClass
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:54:in `block in stub'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:9:in `block (2 levels) in <top (required)>'


  2) Error:
JuanitoMock#test_0005_does not raise an error if expectations are satisfied:
NoMethodError: undefined method `reverse' for nil:NilClass
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:54:in `block in stub'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:49:in `block (2 levels) in <top (required)>'


  3) Error:
JuanitoMock#test_0003_preserves methods that are originally existed:
JuanitoMock::ExpectationNotSatisfied: JuanitoMock::ExpectationNotSatisfied
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:97:in `verify'
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:117:in `each'
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:117:in `reset'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:27:in `block (2 levels) in <top (required)>'


  4) Error:
JuanitoMock#test_0006_allows object to receive messages with arguments:
NoMethodError: undefined method `reverse' for nil:NilClass
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:54:in `block in stub'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:60:in `block (2 levels) in <top (required)>'

6 runs, 2 assertions, 0 failures, 4 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

Yikes. 4 tests failed! Let's take a look at the last one:

JuanitoMock#test_0006_allows object to receive messages with arguments:
NoMethodError: undefined method `reverse' for nil:NilClass
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:54:in `block in stub'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:60:in `block (2 levels) in <top (required)>'

Hmm. Let's look at our implementation again:

@obj.define_singleton_method definition.message do |*arguments|
  @definitions
    .reverse
    .find { |definition| definition.matches(definition.message, *arguments) }
    .call
end

Why is @definitions nil? That's because self has changed, in a singleton method block like this:

@obj.define_singleton_method definition.message do |*arguments|
  ...
end

And because instance variables (@definitions) are looked up on self (which is now @obj and not the outer instance), @definitions is something different (and unintialized) in the block. We call this a closure.

An easy fix would be to create a temporary variable:

module JuanitoMock
  ...

  class Stubber
    ...

    def stub(definition)
      ...

      definitions = @definitions
      @obj.define_singleton_method definition.message do |*arguments|
        definitions
          .reverse
          .find { |definition| definition.matches(definition.message, *arguments) }
          .call
      end
    end

    def reset
      ...
    end
  end

  ...
end

Run the tests again:

$ rake
Run options: --seed 52635

# Running:

..EEEE

Finished in 0.001867s, 3214.3833 runs/s, 1071.4611 assertions/s.

  1) Error:
JuanitoMock#test_0006_allows object to receive messages with arguments:
NoMethodError: undefined method `matches' for #<JuanitoMock::ExpectationDefinition:0x007ff542c9bf00>
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:55:in `block (2 levels) in stub'
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:55:in `each'
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:55:in `find'
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:55:in `block in stub'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:60:in `block (2 levels) in <top (required)>'


  2) Error:
JuanitoMock#test_0005_does not raise an error if expectations are satisfied:
NoMethodError: undefined method `matches' for #<JuanitoMock::ExpectationDefinition:0x007ff542c9b7d0>
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:55:in `block (2 levels) in stub'
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:55:in `each'
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:55:in `find'
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:55:in `block in stub'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:49:in `block (2 levels) in <top (required)>'


  3) Error:
JuanitoMock#test_0003_preserves methods that are originally existed:
JuanitoMock::ExpectationNotSatisfied: JuanitoMock::ExpectationNotSatisfied
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:98:in `verify'
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:118:in `each'
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:118:in `reset'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:27:in `block (2 levels) in <top (required)>'


  4) Error:
JuanitoMock#test_0001_allows an object to receive a message and returns a value:
NoMethodError: undefined method `matches' for #<JuanitoMock::ExpectationDefinition:0x007ff542c9ad30>
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:55:in `block (2 levels) in stub'
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:55:in `each'
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:55:in `find'
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:55:in `block in stub'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:9:in `block (2 levels) in <top (required)>'

6 runs, 2 assertions, 0 failures, 4 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

We got rid of the nil error, and now we have an undefined method matches in ExpectationDefinition!

This is the final step, I promise:

  class ExpectationDefinition
    ...

    def with(*arguments)
      ...
    end

    def matches(message, *arguments)
      message == @message &&
        (@arguments.nil?) || arguments == @arguments
    end

    def call
      ...
    end

    ...
  end

Again, we'll run all the tests:

$ rake
Run options: --seed 14495

# Running:

......

Finished in 0.001514s, 3963.4020 runs/s, 3963.4020 assertions/s.

6 runs, 6 assertions, 0 failures, 0 errors, 0 skips

Now we have ALL OUR TESTS PASSING!

C O N G R A T U L A T I O N S

You've got a basic mocking library!

This library is pretty good now, except with some caveats...

  • with(...) and calling it with different arguments raises NoMethodError
  • define_singleton_method and singleton_class are on Object and so stubbing on BasicObject is not supported
  • private methods are not preserved
  • reset method should be invoked automatically at the end of each test (teardown)

Luckily, RSpec already addressed these and more, so you can just use rspec-mocks.

Further Reading:

Thank you for reading!

Happy Mocking!

Till next time 😘
Juanito Fatas,
Edits by Winston Teo Yong Wei

If you tweet or share this tutorial, don't forget to mention and thank @alindeman!

All credits go to him! I only did the writing here. 😉

Originally posted on jollygoodcode/jollygoodcode.github.io#2.


Any mistake? Send me a Pull Request <3

Share this post