Skip to content

Commit

Permalink
Make doubles more like mockito.
Browse files Browse the repository at this point in the history
Remove allow and assert_exhausted, add verify.
  • Loading branch information
xaviershay committed Jul 26, 2014
1 parent 4d74f07 commit 66b91e2
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 192 deletions.
70 changes: 20 additions & 50 deletions doc/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -169,60 +169,48 @@ def save(message, repository: Repository.new)
# `Repository.new`), and `class_double` when doubling class methods.
let(:repo) { instance_double('Repository') }

# Set expecations on the test double by wrapping it in a call to
# `expect`, and then calling the invocation you are expecting.
#
# `assert_exhausted` will fail unless all expectations were invoked.
# Doubles allow you to selectively verify interactions with them by
# wrapping them in a call to `verify` then calling the invocation you
# expected.
it 'stores a hash document in the repository' do
expect(repo).store(msg: 'hello')
save('hello', repository: repo)
assert_exhausted repo
verify(repo).store(msg: 'hello')
end

# Calling methods that were not expected cause the test to fail. This test
# will fail because the double is not expecting a message with "goodbye" in
# it.
# If a matching method has not been called, the test will fail This test
# will fail because the double did not receive a message with "hello".
it 'stores a hash document in the repository - broken' do
expect_to_fail!

expect(repo).store(msg: 'hello')
save('goodbye', repository: repo)
verify(repo).store(msg: 'hello')
end

# In this case the test fails because the `store` expectation was never
# invoked.
it 'stores a hash document in the repository - broken' do
expect_to_fail!

expect(repo).store(msg: 'hello')
assert_exhausted repo
end

# Invocations can be allowed but not required using `allow`. This test does
# not fail, even though `store` was never called.
# Methods can be pre-emptively expected using `expect`. This has the
# benefit of allowing a return value to be specified.
it 'stores a hash document in the repository' do
allow(repo).store(msg: 'hello')
assert_exhausted repo
expect(repo).store(msg: 'hello') { true }
assert save('hello', repository: repo)
end

# By default, doubling classes that do no exist is allowed. It is assumed
# that the test is being run in isolation so the collaborator, or it has
# not been implemented yet.
#
# If the class does exist, both `expect` and `allow` check invocations
# If the class does exist, both `verify` and `expect` check invocations
# against methods that are actually implemented on the doubled class. This
# test fails because `put` is not a method.
it 'stores a hash document in the repository' do
expect_to_fail!
expect(repo).put(msg: 'hello')
verify(repo).put(msg: 'hello')
end

# If not, any expecation is allowed. It is assumed that this test will be
# run again in the future either once the class is implemented, or as part
# of a larger run that loads all collaborators.
# If the class does exist, any expecation is allowed. It is assumed that
# this test will be run again in the future either once the class is
# implemented, or as part of a larger run that loads all collaborators.
it 'stores a hash document in an alternate repository' do
alt_repo = class_double('RemoteRepository')
allow(alt_repo).put(msg: 'hello')
verify(alt_repo).put(msg: 'hello')
end

# #### Strict mode
Expand All @@ -233,6 +221,8 @@ module Strict
# A cute trick is to disable this by default, and only enable it in full
# test runs. That way individual tests can be executed quickly without
# loading all dependencies.
#
# Strict mode is not enabled in the default XSpec configuration.
extend XSpec.dsl(
evaluator: documentation_stack {
include XSpec::Evaluator::Doubles.with(:strict)
Expand All @@ -244,27 +234,7 @@ module Strict
it 'stores a hash document in an alternate repository' do
expect_to_fail!

alt_repo = class_double('RemoteRepository')
end
end

# #### Auto-verification
module AutoVerify
# `assert_exhausted` can be called automatically on all created doubles
# after a test has run. This is default behaviour. `strict` is only
# enabled here to demonstrate that `with` takes a variable number of
# arguments, it is not actually necessary for auto-verification.
extend XSpec.dsl(
evaluator: documentation_stack {
include XSpec::Evaluator::Doubles.with(:strict, :auto_verify)
}
)

# This test fails because `store` is never called on the double.
it 'stores a hash document in an alternate repository' do
expect_to_fail!

expect(repo).store(msg: 'hello')
class_double('RemoteRepository')
end
end
end
Expand Down
98 changes: 29 additions & 69 deletions lib/xspec/evaluators.rb
Original file line number Diff line number Diff line change
Expand Up @@ -107,20 +107,16 @@ def call(unit_of_work)
[Failure.new(unit_of_work, e.message, e.backtrace)]
end

# It can be configured with a few options:
# It can be configured with the following options:
#
# * `auto_verify` calls `assert_exhausted` on all created doubles after a
# unit of work executes successfully to ensure that all expectations that
# were set were actually called.
# * `strict` forbids doubling of classes that have not been loaded. This
# should generally be enabled when doing a full spec run, and disabled
# when running specs in isolation.
#
# The `with` method returns a module that can be included in a stack.
def self.with(*opts)
modules = [self] + opts.map {|x| {
auto_verify: AutoVerify,
strict: Strict
strict: Strict
}.fetch(x) }


Expand Down Expand Up @@ -162,21 +158,21 @@ def _double(klass, type)
# is desired, it can be supplied as a block, for example:
# `expect(double).some_method(1, 2) { "return value" }`
def expect(obj)
Recorder.new(obj)
Proxy.new(obj, :_expect)
end

# TODO: Fix this
def allow(obj)
Recorder.new(obj)
def verify(obj)
Proxy.new(obj, :_verify)
end

class Recorder
def initialize(double)
class Proxy
def initialize(double, method)
@double = double
@method = method
end

def method_missing(*args, &ret)
@double._expect(args, &(ret || ->{}))
@double.__send__(@method, args, &(ret || ->{}))
end
end

Expand All @@ -188,6 +184,7 @@ class Double < BasicObject
def initialize(klass)
@klass = klass
@expected = []
@received = []
end

def method_missing(*actual_args)
Expand All @@ -198,11 +195,8 @@ def method_missing(*actual_args)
if i
@expected.delete_at(i)[1].call
else
name, rest = *actual_args
::Kernel.raise DoubleFailure, "Unexpectedly received: %s(%s)" % [
name,
[*rest].map(&:inspect).join(", ")
]
@received << actual_args
nil
end
end

Expand All @@ -217,15 +211,22 @@ def _expect(args, &ret)
@expected << [args, ret]
end

def _verify
return if @expected.empty?
def _verify(args)
i = @received.index(args)

::Kernel.raise DoubleFailure, "%s double did not receive:\n%s" % [
@klass.to_s,
@expected.map {|(name, *args), _|
" %s(%s)" % [name, args.map(&:inspect).join(", ")]
}.join("\n")
]
if i
@received.delete_at(i)
else
name, rest = *args
::Kernel.raise DoubleFailure,
"Did not receive: %s(%s)\nDid receive:%s\n" % [
name,
[*rest].map(&:inspect).join(", "),
@received.map {|name, *args|
" %s(%s)" % [name, args.map(&:inspect).join(", ")]
}.join("\n")
]
end
end
end

Expand Down Expand Up @@ -289,50 +290,9 @@ def _double(klass, type)
super
end
end

# An assertion is provided to validate that all expected methods were
# called on a double.
def assert_exhausted(obj)
obj._verify
end

# Most of the time, `assert_exhausted` will not be called directly, since
# the `:auto_verify` option can be used to call it by default on all
# doubles. That option mixes in this `AutoVerify` module to augment
# methods necessary for this behaviour.
module AutoVerify
def initialize
@doubles = []
end

def call(unit_of_work)
result = super

if result.empty?
@doubles.each do |double|
assert_exhausted double
end
end

result
rescue DoubleFailure => e
[Failure.new(unit_of_work, e.message, e.backtrace)]
end

def class_double(klass)
x = super
@doubles << x
x
end

def instance_double(klass)
x = super
@doubles << x
x
end
end
end


# ### RSpec Integration
#
# This RSpec adapter shows two useful techniques: dynamic library loading
Expand All @@ -359,7 +319,7 @@ def call(unit_of_work)

DEFAULT = stack do
include Simple
include Doubles.with(:auto_verify)
include Doubles
end
end
end
Loading

0 comments on commit 66b91e2

Please sign in to comment.