Have you seen the Mock Muack yet?

http://godfat.org/slide/2015-09-11-mock/

Who?

Lin Jen-Shin (godfat)

RubyConf.TW 2010 (Lightning Talk)

RubyConf.TW 2011

  • rest-core — A modular Ruby REST client collection/infrastructure

RubyConf.TW 2012

RubyConf.TW 2013
  • 404 — Conference Not Found

RubyConf.TW 2014

  • RubyQC — A conceptual QuickCheck library for Ruby

RubyConf.TW 2015

  • Have you seen the Mock Muack yet?

Table of Contents

Universe
 
Program
Universe
Proofs
Program
Universe
(Type System)
Program
Universe
(Type System)
Program
Tests

Testing
is part of development process

Not part of the program

Pick the one fits into your team

Software Testing

Test double

A test double is a generic (meta) term used for these objects or procedures.

Test double

A test double is a generic (meta) term buzzword used for these objects or procedures.

Mock object

In object-oriented programming, mock objects are simulated objects that mimic the behavior of real objects in controlled ways.

Table of Contents

When?

Non-deterministic

Current time


require 'time'

class Ticket < Struct.new(:time)
  def expired?
    Time.parse(time) < Time.now
  end
end

    Ticket.new('2015-09-11').expired?

Difficult to reproduce

Network error


require 'socket'

sock = TCPSocket.new('t.co', 80)
sock.write("GET / HTTP/1.0\r\nHost: t.co\r\n\r\n")
begin
  IO.select([sock])
  sock.read_nonblock(256)
rescue IO::WaitReadable
  retry # How to test this clause?
end
sock.close

Too slow

Random bytes


data = File.read('/dev/random', 2**20)

Upload data


require 'socket'
sock = TCPSocket.new('t.co', 80)
sock.write("POST / HTTP/1.0\r\nHost: t.co\r\n\r\n")
sock.write(data)
sock.close

Table of Contents

Muack

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.

Kernel: Hello!


require 'pork/auto'
require 'muack'

describe Kernel do
  include Muack::API
  before{ Muack.reset  }
  after { Muack.verify }

  would 'puts' do
    mock(self).puts(is_a(String)){ ok }
    puts "Hello!"
  end
end

Non-deterministic

Current time


class Ticket < Struct.new(:time)
  def expired?
    Time.parse(time) < Time.now
  end
end



    Ticket.new('2015-09-11').expired?


Non-deterministic

Current time


class Ticket < Struct.new(:time)
  def expired?
    Time.parse(time) < Time.now
  end
end
describe Ticket do
  would 'not expired?' do
    mock(Time).now{ Time.at(0) }
    Ticket.new('2015-09-11').expired?
  end
end

Non-deterministic

Current time


class Ticket < Struct.new(:time)
  def expired?
    Time.parse(time) < Time.now
  end
end
describe Ticket do
  would 'not expired?' do
    mock(Time).now{ Time.at(0) }
    expect(Ticket.new('2015-09-11')).not.expired?
  end
end

Non-deterministic

Current time


describe Ticket do
  would 'expired?' do
    mock(Time).now{ Time.parse('2015-09-12') }
    expect(Ticket.new('2015-09-11')).expired?
  end

  would 'not expired?' do
    mock(Time).now{ Time.at(0) }
    expect(Ticket.new('2015-09-11')).not.expired?
  end
end

Non-deterministic

Current time (with dependency injection)


class Ticket < Struct.new(:time, :time_class)
  def initialize new_time, new_time_class=Time
    super(new_time, new_time_class)
  end

  def expired?
    Time.parse(time) < time_class.now
  end
end

Non-deterministic

Current time (with dependency injection and simple stubs)


describe Ticket do
  would 'expired?' do
    t = Struct.new(:now).new(Time.parse('2015-09-12'))
    expect(Ticket.new('2015-09-11', t)).expired?
  end

  would 'not expired?' do
    t = Struct.new(:now).new(Time.at(0))
    expect(Ticket.new('2015-09-11', t)).not.expired?
  end
end

What are the terms?

Classification between mocks, fakes, and stubs is highly inconsistent across literature.

[...]

Which of the mock, fake, or stub is the simplest is inconsistent, but the simplest always returns pre-arranged responses.

Test double

A test double is a generic (meta) term buzzword used for these objects or procedures.

Mock object

In object-oriented programming, mock objects are simulated objects that mimic the behavior of real objects in controlled ways.

Mocks Aren't Stubs

The Difference Between Mocks and Stubs

Muack

There are also 4 different kinds of mocks in Muack, which are:

Non-deterministic

Current time (with dependency injection and simple stubs)


describe Ticket do
  would 'expired?' do
    t = Struct.new(:now).new(Time.parse('2015-09-12'))
    expect(Ticket.new('2015-09-11', t)).expired?
  end

  would 'not expired?' do
    t = Struct.new(:now).new(Time.at(0))
    expect(Ticket.new('2015-09-11', t)).not.expired?
  end
end

Non-deterministic

Current time (with dependency injection and muack stubs)


describe Ticket do
  would 'expired?' do
    t = stub.now{ Time.parse('2015-09-12') }.object
    expect(Ticket.new('2015-09-11', t)).expired?
  end

  would 'not expired?' do
    t = stub.now{ Time.at(0) }.object
    expect(Ticket.new('2015-09-11', t)).not.expired?
  end
end

Non-deterministic

Current time (with dependency injection and muack mocks)


describe Ticket do
  would 'expired?' do
    t = mock.now{ Time.parse('2015-09-12') }.object
    expect(Ticket.new('2015-09-11', t)).expired?
  end

  would 'not expired?' do
    t = mock.now{ Time.at(0) }.object
    expect(Ticket.new('2015-09-11', t)).not.expired?
  end
end

Non-deterministic

Current time (with dependency injection and muack spies)


describe Ticket do
  would 'expired?' do
    t = stub.now{ Time.parse('2015-09-12') }.object
    expect(Ticket.new('2015-09-11', t)).expired?
    spy(t).now


  end
end

Non-deterministic

Current time (with muack spies)


describe Ticket do
  would 'expired?' do
    stub(Time).now{ Time.parse('2015-09-12') }
    expect(Ticket.new('2015-09-11')).expired?
    spy(Time).now
    Muack.verify
    Time.now # => real time here
  end
end

Non-deterministic

Current time (with muack coats)


describe Ticket do
  would 'expired?' do
    coat(Time).now{ Time.parse('2015-09-12') }
    expect(Ticket.new('2015-09-11')).expired?


    Time.now # => real time here
  end
end

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

Muack Partial Mode

Partial mode is not really a mode, but a combination of using proxy mode and the pattern matching mechanism specialized in stubs.

Muack any_instance_of

Well, sort of a hack to avoid dependency injection yet making it testable.

Dependency Injection Disadvantages

  • It forces complexity to move out of classes and into the linkages between classes which might not always be desirable or easily managed.

Muack Modifiers

Muack Arguments Verifiers

Muack Arguments Verifiers

Muack Caveat

Table of Contents

Trade-Offs

Test Isolation -- without mocks

Advantages:


Disadvantages:

  • Test not isolated -- more complex runtime & harder to track down bugs

Test Isolation -- with mocks

Advantages:


Disadvantages:

  • Need to implement with mocks -- duplicated implementation

Trade-Offs

Without Dependency Injection

Advantages:


Disadvantages:

  • Dependencies are more coupled and unclear

With Dependency Injection

Advantages:


Disadvantages:

  • It forces complexity to move out of classes and into the linkages between classes which might not always be desirable or easily managed

Table of Contents

Auto-mocking with webmock


WebMock.disable_net_connect!

copy :automock do
  before do
    stub(Notify).new_notification.
      with_any_args.peek_args do |*args|
        stub_firebase("notify/#{args.first.fid}")
        args
      end
  end
end

describe Notify do
  paste :automock
end

Truncate only inserted tables


Model = Class.new(Sequel::Model)
copy :model do
  def inserted; @inserted ||= {}; end
  before do
    test = self
    stub(any_instance_of(Model))._insert.
      peek_return(:instance_exec => true) do |r|
        test.inserted[self.class.table_name] = true
        r
      end
  end
  after do
    Model.truncate_all_tables!(inserted.keys)
  end
end

As a development runtime static typing system


Food = Class.new
User = Class.new(Struct.new(:food))

Muack::API.module_eval do
  any_instance_of(User) do |user|
    stub(user).food = is_a(Food) # proxy mode
  end
end

u, f = User.new, Food.new
u.food = f # ok
u.food = 1 # raise Muack::Unexpected

As a mocky patching library


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?
      # There's no show page for unsaved records
      [nil, {}]
    else
      args # Bypass arguments
    end
  end

Bibliography

Q?

https://github.com/godfat/muack







gem install muack