mirror of
https://gitea.invidious.io/iv-org/shard-spectator.git
synced 2024-08-15 00:53:35 +00:00
Merge branch 'mocks-and-doubles' into 'release/0.9'
Mocks and doubles See merge request arctic-fox/spectator!16
This commit is contained in:
commit
2cb34f70fd
52 changed files with 1362 additions and 41 deletions
11
README.md
11
README.md
|
@ -311,11 +311,11 @@ Items not marked as completed may have partial implementations.
|
||||||
- [ ] Compound - `and`, `or`
|
- [ ] Compound - `and`, `or`
|
||||||
- [ ] Mocks and Doubles
|
- [ ] Mocks and Doubles
|
||||||
- [ ] Mocks (Stub real types) - `mock TYPE { }`
|
- [ ] Mocks (Stub real types) - `mock TYPE { }`
|
||||||
- [ ] Doubles (Stand-ins for real types) - `double NAME { }`
|
- [X] Doubles (Stand-ins for real types) - `double NAME { }`
|
||||||
- [ ] Method stubs - `allow().to receive()`, `allow().to receive().and_return()`
|
- [X] Method stubs - `allow().to receive()`, `allow().to receive().and_return()`
|
||||||
- [ ] Spies - `expect().to receive()`
|
- [ ] Spies - `expect().to receive()`
|
||||||
- [ ] Message expectations - `expect().to receive().at_least()`
|
- [X] Message expectations - `expect().to receive().at_least()`
|
||||||
- [ ] Argument expectations - `expect().to receive().with()`
|
- [X] Argument expectations - `expect().to receive().with()`
|
||||||
- [ ] Message ordering - `expect().to receive().ordered`
|
- [ ] Message ordering - `expect().to receive().ordered`
|
||||||
- [ ] Null doubles
|
- [ ] Null doubles
|
||||||
- [ ] Runner
|
- [ ] Runner
|
||||||
|
@ -326,11 +326,12 @@ Items not marked as completed may have partial implementations.
|
||||||
- [X] Dry run - for validation and checking formatted output
|
- [X] Dry run - for validation and checking formatted output
|
||||||
- [X] Config block in `spec_helper.cr`
|
- [X] Config block in `spec_helper.cr`
|
||||||
- [X] Config file - `.spectator`
|
- [X] Config file - `.spectator`
|
||||||
- [X] Reporter and formatting
|
- [ ] Reporter and formatting
|
||||||
- [X] RSpec/Crystal Spec default
|
- [X] RSpec/Crystal Spec default
|
||||||
- [X] JSON
|
- [X] JSON
|
||||||
- [X] JUnit
|
- [X] JUnit
|
||||||
- [X] TAP
|
- [X] TAP
|
||||||
|
- [ ] HTML
|
||||||
|
|
||||||
### How it Works (in a nutshell)
|
### How it Works (in a nutshell)
|
||||||
|
|
||||||
|
|
15
src/spectator/anything.cr
Normal file
15
src/spectator/anything.cr
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
module Spectator
|
||||||
|
struct Anything
|
||||||
|
def ==(other)
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def ===(other)
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def =~(other)
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -683,6 +683,11 @@ module Spectator
|
||||||
expect {{block}}.to raise_error({{type}}, {{message}})
|
expect {{block}}.to raise_error({{type}}, {{message}})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
macro have_received(method)
|
||||||
|
%test_value = ::Spectator::TestValue.new(({{method.id.symbolize}}), {{method.id.stringify}})
|
||||||
|
::Spectator::Matchers::ReceiveMatcher.new(%test_value)
|
||||||
|
end
|
||||||
|
|
||||||
# Used to create predicate matchers.
|
# Used to create predicate matchers.
|
||||||
# Any missing method that starts with 'be_' or 'have_' will be handled.
|
# Any missing method that starts with 'be_' or 'have_' will be handled.
|
||||||
# All other method names will be ignored and raise a compile-time error.
|
# All other method names will be ignored and raise a compile-time error.
|
||||||
|
|
173
src/spectator/dsl/mocks.cr
Normal file
173
src/spectator/dsl/mocks.cr
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
require "../mocks"
|
||||||
|
|
||||||
|
module Spectator::DSL
|
||||||
|
macro double(name = "Anonymous", **stubs, &block)
|
||||||
|
{% if name.is_a?(StringLiteral) %}
|
||||||
|
anonymous_double({{name}}, {{stubs.double_splat}})
|
||||||
|
{% else %}
|
||||||
|
{%
|
||||||
|
safe_name = name.id.symbolize.gsub(/\W/, "_").id
|
||||||
|
type_name = "Double#{safe_name}".id
|
||||||
|
%}
|
||||||
|
|
||||||
|
{% if block.is_a?(Nop) %}
|
||||||
|
create_double({{type_name}}, {{name}}, {{stubs.double_splat}})
|
||||||
|
{% else %}
|
||||||
|
define_double({{type_name}}, {{name}}, {{stubs.double_splat}}) {{block}}
|
||||||
|
{% end %}
|
||||||
|
{% end %}
|
||||||
|
end
|
||||||
|
|
||||||
|
macro create_double(type_name, name, **stubs)
|
||||||
|
{% type_name.resolve? || raise("Could not find a double labeled #{name}") %}
|
||||||
|
|
||||||
|
{{type_name}}.new.tap do |%double|
|
||||||
|
{% for name, value in stubs %}
|
||||||
|
allow(%double).to receive({{name.id}}).and_return({{value}})
|
||||||
|
{% end %}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
macro define_double(type_name, name, **stubs, &block)
|
||||||
|
{% begin %}
|
||||||
|
{% if (name.is_a?(Path) || name.is_a?(Generic)) && (resolved = name.resolve?) %}
|
||||||
|
verify_double({{name}})
|
||||||
|
class {{type_name}} < ::Spectator::Mocks::VerifyingDouble(::{{resolved.id}})
|
||||||
|
{% else %}
|
||||||
|
class {{type_name}} < ::Spectator::Mocks::Double
|
||||||
|
def initialize(null = false)
|
||||||
|
super({{name.id.stringify}}, null)
|
||||||
|
end
|
||||||
|
{% end %}
|
||||||
|
|
||||||
|
def as_null_object
|
||||||
|
{{type_name}}.new(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO: Do something with **stubs?
|
||||||
|
|
||||||
|
{{block.body}}
|
||||||
|
end
|
||||||
|
{% end %}
|
||||||
|
end
|
||||||
|
|
||||||
|
def anonymous_double(name = "Anonymous", **stubs)
|
||||||
|
Mocks::AnonymousDouble.new(name, stubs)
|
||||||
|
end
|
||||||
|
|
||||||
|
macro null_double(name, **stubs, &block)
|
||||||
|
{% if name.is_a?(StringLiteral) %}
|
||||||
|
anonymous_null_double({{name}}, {{stubs.double_splat}})
|
||||||
|
{% else %}
|
||||||
|
{%
|
||||||
|
safe_name = name.id.symbolize.gsub(/\W/, "_").id
|
||||||
|
type_name = "Double#{safe_name}".id
|
||||||
|
%}
|
||||||
|
|
||||||
|
{% if block.is_a?(Nop) %}
|
||||||
|
create_null_double({{type_name}}, {{name}}, {{stubs.double_splat}})
|
||||||
|
{% else %}
|
||||||
|
define_null_double({{type_name}}, {{name}}, {{stubs.double_splat}}) {{block}}
|
||||||
|
{% end %}
|
||||||
|
{% end %}
|
||||||
|
end
|
||||||
|
|
||||||
|
macro create_null_double(type_name, name, **stubs)
|
||||||
|
{% type_name.resolve? || raise("Could not find a double labeled #{name}") %}
|
||||||
|
|
||||||
|
{{type_name}}.new(true).tap do |%double|
|
||||||
|
{% for name, value in stubs %}
|
||||||
|
allow(%double).to receive({{name.id}}).and_return({{value}})
|
||||||
|
{% end %}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
macro define_null_double(type_name, name, **stubs, &block)
|
||||||
|
class {{type_name}} < ::Spectator::Mocks::Double
|
||||||
|
def initialize(null = true)
|
||||||
|
super({{name.id.stringify}}, null)
|
||||||
|
end
|
||||||
|
|
||||||
|
def as_null_object
|
||||||
|
{{type_name}}.new(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO: Do something with **stubs?
|
||||||
|
|
||||||
|
{{block.body}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def anonymous_null_double(name = "Anonymous", **stubs)
|
||||||
|
AnonymousNullDouble.new(name, stubs)
|
||||||
|
end
|
||||||
|
|
||||||
|
macro mock(name, &block)
|
||||||
|
{% resolved = name.resolve
|
||||||
|
type = if resolved < Reference
|
||||||
|
:class
|
||||||
|
elsif resolved < Value
|
||||||
|
:struct
|
||||||
|
else
|
||||||
|
:module
|
||||||
|
end %}
|
||||||
|
{% begin %}
|
||||||
|
{{type.id}} ::{{resolved.id}}
|
||||||
|
include ::Spectator::Mocks::Stubs
|
||||||
|
|
||||||
|
{{block.body}}
|
||||||
|
end
|
||||||
|
{% end %}
|
||||||
|
end
|
||||||
|
|
||||||
|
macro verify_double(name, &block)
|
||||||
|
{% resolved = name.resolve
|
||||||
|
type = if resolved < Reference
|
||||||
|
:class
|
||||||
|
elsif resolved < Value
|
||||||
|
:struct
|
||||||
|
else
|
||||||
|
:module
|
||||||
|
end %}
|
||||||
|
{% begin %}
|
||||||
|
{{type.id}} ::{{resolved.id}}
|
||||||
|
include ::Spectator::Mocks::Reflection
|
||||||
|
|
||||||
|
macro finished
|
||||||
|
_spectator_reflect
|
||||||
|
end
|
||||||
|
end
|
||||||
|
{% end %}
|
||||||
|
end
|
||||||
|
|
||||||
|
def allow(thing)
|
||||||
|
Mocks::Allow.new(thing)
|
||||||
|
end
|
||||||
|
|
||||||
|
def allow_any_instance_of(type : T.class) forall T
|
||||||
|
Mocks::AllowAnyInstance(T).new
|
||||||
|
end
|
||||||
|
|
||||||
|
macro expect_any_instance_of(type, _source_file = __FILE__, _source_line = __LINE__)
|
||||||
|
%source = ::Spectator::Source.new({{_source_file}}, {{_source_line}})
|
||||||
|
::Spectator::Mocks::ExpectAnyInstance({{type}}).new(%source)
|
||||||
|
end
|
||||||
|
|
||||||
|
macro receive(method_name, _source_file = __FILE__, _source_line = __LINE__, &block)
|
||||||
|
%source = ::Spectator::Source.new({{_source_file}}, {{_source_line}})
|
||||||
|
{% if block.is_a?(Nop) %}
|
||||||
|
::Spectator::Mocks::NilMethodStub.new({{method_name.id.symbolize}}, %source)
|
||||||
|
{% else %}
|
||||||
|
::Spectator::Mocks::ProcMethodStub.create({{method_name.id.symbolize}}, %source) { {{block.body}} }
|
||||||
|
{% end %}
|
||||||
|
end
|
||||||
|
|
||||||
|
macro receive_messages(_source_file = __FILE__, _source_line = __LINE__, **stubs)
|
||||||
|
%source = ::Spectator::Source.new({{_source_file}}, {{_source_line}})
|
||||||
|
%stubs = [] of ::Spectator::Mocks::MethodStub
|
||||||
|
{% for name, value in stubs %}
|
||||||
|
%stubs << ::Spectator::Mocks::ValueMethodStub.new({{name.id.symbolize}}, %source, {{value}})
|
||||||
|
{% end %}
|
||||||
|
%stubs
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,17 +1,17 @@
|
||||||
module Spectator
|
module Spectator
|
||||||
module DSL
|
module DSL
|
||||||
macro let(name, &block)
|
macro let(name, &block)
|
||||||
def %value
|
|
||||||
{{block.body}}
|
|
||||||
end
|
|
||||||
|
|
||||||
@%wrapper : ::Spectator::ValueWrapper?
|
@%wrapper : ::Spectator::ValueWrapper?
|
||||||
|
|
||||||
|
def {{name.id}}
|
||||||
|
{{block.body}}
|
||||||
|
end
|
||||||
|
|
||||||
def {{name.id}}
|
def {{name.id}}
|
||||||
if (wrapper = @%wrapper)
|
if (wrapper = @%wrapper)
|
||||||
wrapper.as(::Spectator::TypedValueWrapper(typeof(%value))).value
|
wrapper.as(::Spectator::TypedValueWrapper(typeof(previous_def))).value
|
||||||
else
|
else
|
||||||
%value.tap do |value|
|
previous_def.tap do |value|
|
||||||
@%wrapper = ::Spectator::TypedValueWrapper.new(value)
|
@%wrapper = ::Spectator::TypedValueWrapper.new(value)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -35,6 +35,10 @@ module Spectator
|
||||||
@example_count = children.sum(&.example_count)
|
@example_count = children.sum(&.example_count)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def double(id, sample_values)
|
||||||
|
@doubles[id].build(sample_values)
|
||||||
|
end
|
||||||
|
|
||||||
getter context
|
getter context
|
||||||
|
|
||||||
def initialize(@context : TestContext)
|
def initialize(@context : TestContext)
|
||||||
|
|
|
@ -24,6 +24,17 @@ module Spectator::Expectations
|
||||||
report(match_data)
|
report(match_data)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def to(stub : Mocks::MethodStub) : Nil
|
||||||
|
Harness.current.mocks.expect(@actual.value, stub)
|
||||||
|
value = TestValue.new(stub.name, stub.to_s)
|
||||||
|
matcher = Matchers::ReceiveMatcher.new(value, stub.arguments?)
|
||||||
|
to_eventually(matcher)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to(stubs : Enumerable(Mocks::MethodStub)) : Nil
|
||||||
|
stubs.each { |stub| to(stub) }
|
||||||
|
end
|
||||||
|
|
||||||
# Asserts that some criteria defined by the matcher is not satisfied.
|
# Asserts that some criteria defined by the matcher is not satisfied.
|
||||||
# This is effectively the opposite of `#to`.
|
# This is effectively the opposite of `#to`.
|
||||||
def to_not(matcher) : Nil
|
def to_not(matcher) : Nil
|
||||||
|
@ -31,6 +42,16 @@ module Spectator::Expectations
|
||||||
report(match_data)
|
report(match_data)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def to_not(stub : Mocks::MethodStub) : Nil
|
||||||
|
value = TestValue.new(stub.name, stub.to_s)
|
||||||
|
matcher = Matchers::ReceiveMatcher.new(value, stub.arguments?)
|
||||||
|
to_never(matcher)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_not(stubs : Enumerable(Mocks::MethodStub)) : Nil
|
||||||
|
stubs.each { |stub| to_not(stub) }
|
||||||
|
end
|
||||||
|
|
||||||
# ditto
|
# ditto
|
||||||
@[AlwaysInline]
|
@[AlwaysInline]
|
||||||
def not_to(matcher) : Nil
|
def not_to(matcher) : Nil
|
||||||
|
|
|
@ -13,7 +13,7 @@ module Spectator::Formatting
|
||||||
end
|
end
|
||||||
|
|
||||||
# Creates a colorized version of the comment.
|
# Creates a colorized version of the comment.
|
||||||
def self.color(text : T) forall T
|
def self.color(text)
|
||||||
Color.comment(new(text))
|
Color.comment(new(text))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
require "./mocks/registry"
|
||||||
|
|
||||||
module Spectator
|
module Spectator
|
||||||
# Helper class that acts as a gateway between example code and the test framework.
|
# Helper class that acts as a gateway between example code and the test framework.
|
||||||
# Every example must be invoked by passing it to `#run`.
|
# Every example must be invoked by passing it to `#run`.
|
||||||
|
@ -34,6 +36,8 @@ module Spectator
|
||||||
# Retrieves the current running example.
|
# Retrieves the current running example.
|
||||||
getter example : Example
|
getter example : Example
|
||||||
|
|
||||||
|
getter mocks : Mocks::Registry
|
||||||
|
|
||||||
# Retrieves the group for the current running example.
|
# Retrieves the group for the current running example.
|
||||||
def group
|
def group
|
||||||
example.group
|
example.group
|
||||||
|
@ -66,6 +70,7 @@ module Spectator
|
||||||
# The example the harness is for should be passed in.
|
# The example the harness is for should be passed in.
|
||||||
private def initialize(@example)
|
private def initialize(@example)
|
||||||
@reporter = Expectations::ExpectationReporter.new
|
@reporter = Expectations::ExpectationReporter.new
|
||||||
|
@mocks = Mocks::Registry.new(@example.group.context)
|
||||||
@deferred = Deque(->).new
|
@deferred = Deque(->).new
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -29,6 +29,8 @@ require "./example_group"
|
||||||
require "./nested_example_group"
|
require "./nested_example_group"
|
||||||
require "./root_example_group"
|
require "./root_example_group"
|
||||||
|
|
||||||
|
require "./mocks"
|
||||||
|
|
||||||
require "./config"
|
require "./config"
|
||||||
require "./config_builder"
|
require "./config_builder"
|
||||||
require "./config_source"
|
require "./config_source"
|
||||||
|
|
|
@ -26,7 +26,7 @@ module Spectator::Matchers
|
||||||
match_data = matcher.match(element)
|
match_data = matcher.match(element)
|
||||||
break match_data unless match_data.matched?
|
break match_data unless match_data.matched?
|
||||||
end
|
end
|
||||||
found ? found : SuccessfulMatchData.new
|
found || SuccessfulMatchData.new
|
||||||
end
|
end
|
||||||
|
|
||||||
# Negated matching for this matcher is not supported.
|
# Negated matching for this matcher is not supported.
|
||||||
|
|
|
@ -63,12 +63,12 @@ module Spectator::Matchers
|
||||||
end
|
end
|
||||||
|
|
||||||
# Specifies what the resulting value of the expression must be.
|
# Specifies what the resulting value of the expression must be.
|
||||||
def to(value : T) forall T
|
def to(value)
|
||||||
ChangeExactMatcher.new(@expression, @expected, value)
|
ChangeExactMatcher.new(@expression, @expected, value)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Specifies what the resulting value of the expression should change by.
|
# Specifies what the resulting value of the expression should change by.
|
||||||
def by(amount : T) forall T
|
def by(amount)
|
||||||
ChangeExactMatcher.new(@expression, @expected, @expected + value)
|
ChangeExactMatcher.new(@expression, @expected, @expected + value)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -49,27 +49,27 @@ module Spectator::Matchers
|
||||||
end
|
end
|
||||||
|
|
||||||
# Specifies what the initial value of the expression must be.
|
# Specifies what the initial value of the expression must be.
|
||||||
def from(value : T) forall T
|
def from(value)
|
||||||
ChangeFromMatcher.new(@expression, value)
|
ChangeFromMatcher.new(@expression, value)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Specifies what the resulting value of the expression must be.
|
# Specifies what the resulting value of the expression must be.
|
||||||
def to(value : T) forall T
|
def to(value)
|
||||||
ChangeToMatcher.new(@expression, value)
|
ChangeToMatcher.new(@expression, value)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Specifies that t he resulting value must be some amount different.
|
# Specifies that t he resulting value must be some amount different.
|
||||||
def by(amount : T) forall T
|
def by(amount)
|
||||||
ChangeRelativeMatcher.new(@expression, "by #{amount}") { |before, after| amount == after - before }
|
ChangeRelativeMatcher.new(@expression, "by #{amount}") { |before, after| amount == after - before }
|
||||||
end
|
end
|
||||||
|
|
||||||
# Specifies that the resulting value must be at least some amount different.
|
# Specifies that the resulting value must be at least some amount different.
|
||||||
def by_at_least(minimum : T) forall T
|
def by_at_least(minimum)
|
||||||
ChangeRelativeMatcher.new(@expression, "by at least #{minimum}") { |before, after| minimum <= after - before }
|
ChangeRelativeMatcher.new(@expression, "by at least #{minimum}") { |before, after| minimum <= after - before }
|
||||||
end
|
end
|
||||||
|
|
||||||
# Specifies that the resulting value must be at most some amount different.
|
# Specifies that the resulting value must be at most some amount different.
|
||||||
def by_at_most(maximum : T) forall T
|
def by_at_most(maximum)
|
||||||
ChangeRelativeMatcher.new(@expression, "by at most #{maximum}") { |before, after| maximum >= after - before }
|
ChangeRelativeMatcher.new(@expression, "by at most #{maximum}") { |before, after| maximum >= after - before }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -57,12 +57,12 @@ module Spectator::Matchers
|
||||||
end
|
end
|
||||||
|
|
||||||
# Specifies what the initial value of the expression must be.
|
# Specifies what the initial value of the expression must be.
|
||||||
def from(value : T) forall T
|
def from(value)
|
||||||
ChangeExactMatcher.new(@expression, value, @expected)
|
ChangeExactMatcher.new(@expression, value, @expected)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Specifies how much the initial value should change by.
|
# Specifies how much the initial value should change by.
|
||||||
def by(amount : T) forall T
|
def by(amount)
|
||||||
ChangeExactMatcher.new(@expression, @expected - amount, @expected)
|
ChangeExactMatcher.new(@expression, @expected - amount, @expected)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,10 @@ module Spectator::Matchers
|
||||||
# Additional information from the match that can be used to debug the problem.
|
# Additional information from the match that can be used to debug the problem.
|
||||||
getter values : Array(Tuple(Symbol, String))
|
getter values : Array(Tuple(Symbol, String))
|
||||||
|
|
||||||
|
# Creates the match data.
|
||||||
|
def initialize(@failure_message, @values)
|
||||||
|
end
|
||||||
|
|
||||||
# Creates the match data.
|
# Creates the match data.
|
||||||
def initialize(@failure_message, **values)
|
def initialize(@failure_message, **values)
|
||||||
@values = values.to_a
|
@values = values.to_a
|
||||||
|
|
126
src/spectator/matchers/receive_matcher.cr
Normal file
126
src/spectator/matchers/receive_matcher.cr
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
require "../mocks"
|
||||||
|
require "./standard_matcher"
|
||||||
|
|
||||||
|
module Spectator::Matchers
|
||||||
|
struct ReceiveMatcher < StandardMatcher
|
||||||
|
alias Range = ::Range(Int32, Int32) | ::Range(Nil, Int32) | ::Range(Int32, Nil)
|
||||||
|
|
||||||
|
def initialize(@expected : TestExpression(Symbol), @args : Mocks::Arguments? = nil, @range : Range? = nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
def description : String
|
||||||
|
range = @range
|
||||||
|
"received message #{@expected.label} #{range ? "#{humanize_range(range)} time(s)" : "At least once"} with #{@args || "any arguments"}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def match?(actual : TestExpression(T)) : Bool forall T
|
||||||
|
calls = Harness.current.mocks.calls_for(actual.value, @expected.value)
|
||||||
|
calls.select! { |call| @args === call.args } if @args
|
||||||
|
if (range = @range)
|
||||||
|
range.includes?(calls.size)
|
||||||
|
else
|
||||||
|
!calls.empty?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def failure_message(actual : TestExpression(T)) : String forall T
|
||||||
|
range = @range
|
||||||
|
"#{actual.label} did not receive #{@expected.label} #{range ? "#{humanize_range(range)} time(s)" : "at least once"} with #{@args || "any arguments"}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def failure_message_when_negated(actual) : String
|
||||||
|
range = @range
|
||||||
|
"#{actual.label} received #{@expected.label} #{range ? "#{humanize_range(range)} time(s)" : "at least once"} with #{@args || "any arguments"}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def values(actual : TestExpression(T)) forall T
|
||||||
|
calls = Harness.current.mocks.calls_for(actual.value, @expected.value)
|
||||||
|
calls.select! { |call| @args === call.args } if @args
|
||||||
|
range = @range
|
||||||
|
{
|
||||||
|
expected: "#{range ? "#{humanize_range(range)} time(s)" : "At least once"} with #{@args || "any arguments"}",
|
||||||
|
received: "#{calls.size} time(s)",
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def negated_values(actual : TestExpression(T)) forall T
|
||||||
|
calls = Harness.current.mocks.calls_for(actual.value, @expected.value)
|
||||||
|
calls.select! { |call| @args === call.args } if @args
|
||||||
|
range = @range
|
||||||
|
{
|
||||||
|
expected: "#{range ? "Not #{humanize_range(range)} time(s)" : "Never"} with #{@args || "any arguments"}",
|
||||||
|
received: "#{calls.size} time(s)",
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def with(*args, **opts)
|
||||||
|
args = Mocks::GenericArguments.new(args, opts)
|
||||||
|
ReceiveMatcher.new(@expected, args, @range)
|
||||||
|
end
|
||||||
|
|
||||||
|
def once
|
||||||
|
ReceiveMatcher.new(@expected, @args, (1..1))
|
||||||
|
end
|
||||||
|
|
||||||
|
def twice
|
||||||
|
ReceiveMatcher.new(@expected, @args, (2..2))
|
||||||
|
end
|
||||||
|
|
||||||
|
def exactly(count)
|
||||||
|
Count.new(@expected, @args, (count..count))
|
||||||
|
end
|
||||||
|
|
||||||
|
def at_least(count)
|
||||||
|
Count.new(@expected, @args, (count..))
|
||||||
|
end
|
||||||
|
|
||||||
|
def at_most(count)
|
||||||
|
Count.new(@expected, @args, (..count))
|
||||||
|
end
|
||||||
|
|
||||||
|
def at_least_once
|
||||||
|
at_least(1).times
|
||||||
|
end
|
||||||
|
|
||||||
|
def at_least_twice
|
||||||
|
at_least(2).times
|
||||||
|
end
|
||||||
|
|
||||||
|
def at_most_once
|
||||||
|
at_most(1).times
|
||||||
|
end
|
||||||
|
|
||||||
|
def at_most_twice
|
||||||
|
at_most(2).times
|
||||||
|
end
|
||||||
|
|
||||||
|
def humanize_range(range : Range)
|
||||||
|
if (min = range.begin)
|
||||||
|
if (max = range.end)
|
||||||
|
if min == max
|
||||||
|
min
|
||||||
|
else
|
||||||
|
"#{min} to #{max}"
|
||||||
|
end
|
||||||
|
else
|
||||||
|
"At least #{min}"
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if (max = range.end)
|
||||||
|
"At most #{max}"
|
||||||
|
else
|
||||||
|
raise "Unexpected endless range"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private struct Count
|
||||||
|
def initialize(@expected : TestExpression(Symbol), @args : Mocks::Arguments?, @range : Range)
|
||||||
|
end
|
||||||
|
|
||||||
|
def times
|
||||||
|
ReceiveMatcher.new(@expected, @args, @range)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
126
src/spectator/matchers/receive_type_matcher.cr
Normal file
126
src/spectator/matchers/receive_type_matcher.cr
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
require "../mocks"
|
||||||
|
require "./standard_matcher"
|
||||||
|
|
||||||
|
module Spectator::Matchers
|
||||||
|
struct ReceiveTypeMatcher < StandardMatcher
|
||||||
|
alias Range = ::Range(Int32, Int32) | ::Range(Nil, Int32) | ::Range(Int32, Nil)
|
||||||
|
|
||||||
|
def initialize(@expected : TestExpression(Symbol), @args : Mocks::Arguments? = nil, @range : Range? = nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
def description : String
|
||||||
|
range = @range
|
||||||
|
"received message #{@expected.label} #{range ? "#{humanize_range(range)} time(s)" : "At least once"} with #{@args || "any arguments"}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def match?(actual : TestExpression(T)) : Bool forall T
|
||||||
|
calls = Harness.current.mocks.calls_for_type(actual.value, @expected.value)
|
||||||
|
calls.select! { |call| @args === call.args } if @args
|
||||||
|
if (range = @range)
|
||||||
|
range.includes?(calls.size)
|
||||||
|
else
|
||||||
|
!calls.empty?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def failure_message(actual : TestExpression(T)) : String forall T
|
||||||
|
range = @range
|
||||||
|
"#{actual.label} did not receive #{@expected.label} #{range ? "#{humanize_range(range)} time(s)" : "at least once"} with #{@args || "any arguments"}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def failure_message_when_negated(actual : TestExpression(T)) : String forall T
|
||||||
|
range = @range
|
||||||
|
"#{actual.label} received #{@expected.label} #{range ? "#{humanize_range(range)} time(s)" : "at least once"} with #{@args || "any arguments"}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def values(actual : TestExpression(T)) forall T
|
||||||
|
calls = Harness.current.mocks.calls_for_type(T, @expected.value)
|
||||||
|
calls.select! { |call| @args === call.args } if @args
|
||||||
|
range = @range
|
||||||
|
{
|
||||||
|
expected: "#{range ? "#{humanize_range(range)} time(s)" : "At least once"} with #{@args || "any arguments"}",
|
||||||
|
received: "#{calls.size} time(s)",
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def negated_values(actual : TestExpression(T)) forall T
|
||||||
|
calls = Harness.current.mocks.calls_for_type(T, @expected.value)
|
||||||
|
calls.select! { |call| @args === call.args } if @args
|
||||||
|
range = @range
|
||||||
|
{
|
||||||
|
expected: "#{range ? "Not #{humanize_range(range)} time(s)" : "Never"} with #{@args || "any arguments"}",
|
||||||
|
received: "#{calls.size} time(s)",
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def with(*args, **opts)
|
||||||
|
args = Mocks::GenericArguments.new(args, opts)
|
||||||
|
ReceiveTypeMatcher.new(@expected, args, @range)
|
||||||
|
end
|
||||||
|
|
||||||
|
def once
|
||||||
|
ReceiveTypeMatcher.new(@expected, @args, (1..1))
|
||||||
|
end
|
||||||
|
|
||||||
|
def twice
|
||||||
|
ReceiveTypeMatcher.new(@expected, @args, (2..2))
|
||||||
|
end
|
||||||
|
|
||||||
|
def exactly(count)
|
||||||
|
Count.new(@expected, @args, (count..count))
|
||||||
|
end
|
||||||
|
|
||||||
|
def at_least(count)
|
||||||
|
Count.new(@expected, @args, (count..))
|
||||||
|
end
|
||||||
|
|
||||||
|
def at_most(count)
|
||||||
|
Count.new(@expected, @args, (..count))
|
||||||
|
end
|
||||||
|
|
||||||
|
def at_least_once
|
||||||
|
at_least(1).times
|
||||||
|
end
|
||||||
|
|
||||||
|
def at_least_twice
|
||||||
|
at_least(2).times
|
||||||
|
end
|
||||||
|
|
||||||
|
def at_most_once
|
||||||
|
at_most(1).times
|
||||||
|
end
|
||||||
|
|
||||||
|
def at_most_twice
|
||||||
|
at_most(2).times
|
||||||
|
end
|
||||||
|
|
||||||
|
def humanize_range(range : Range)
|
||||||
|
if (min = range.begin)
|
||||||
|
if (max = range.end)
|
||||||
|
if min == max
|
||||||
|
min
|
||||||
|
else
|
||||||
|
"#{min} to #{max}"
|
||||||
|
end
|
||||||
|
else
|
||||||
|
"At least #{min}"
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if (max = range.end)
|
||||||
|
"At most #{max}"
|
||||||
|
else
|
||||||
|
raise "Unexpected endless range"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private struct Count
|
||||||
|
def initialize(@expected : TestExpression(Symbol), @args : Mocks::Arguments?, @range : Range)
|
||||||
|
end
|
||||||
|
|
||||||
|
def times
|
||||||
|
ReceiveTypeMatcher.new(@expected, @args, @range)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -27,7 +27,7 @@ module Spectator::Matchers
|
||||||
if match?(actual)
|
if match?(actual)
|
||||||
SuccessfulMatchData.new
|
SuccessfulMatchData.new
|
||||||
else
|
else
|
||||||
FailedMatchData.new(failure_message(actual), **values(actual))
|
FailedMatchData.new(failure_message(actual), values(actual).to_a)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ module Spectator::Matchers
|
||||||
if does_not_match?(actual)
|
if does_not_match?(actual)
|
||||||
SuccessfulMatchData.new
|
SuccessfulMatchData.new
|
||||||
else
|
else
|
||||||
FailedMatchData.new(failure_message_when_negated(actual), **negated_values(actual))
|
FailedMatchData.new(failure_message_when_negated(actual), negated_values(actual).to_a)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ module Spectator::Matchers
|
||||||
# The message should typically only contain the test expression labels.
|
# The message should typically only contain the test expression labels.
|
||||||
# Actual values should be returned by `#values`.
|
# Actual values should be returned by `#values`.
|
||||||
private def failure_message_when_negated(actual : TestExpression(T)) : String forall T
|
private def failure_message_when_negated(actual : TestExpression(T)) : String forall T
|
||||||
{% raise "Negation with #{@type.name} is not supported." %}
|
raise "Negation with #{self.class} is not supported."
|
||||||
end
|
end
|
||||||
|
|
||||||
# Checks whether the matcher is satisifed with the expression given to it.
|
# Checks whether the matcher is satisifed with the expression given to it.
|
||||||
|
|
|
@ -27,7 +27,7 @@ module Spectator::Matchers
|
||||||
# ```
|
# ```
|
||||||
# expect(0).to be < 1
|
# expect(0).to be < 1
|
||||||
# ```
|
# ```
|
||||||
def <(value : ExpectedType) forall ExpectedType
|
def <(value)
|
||||||
expected = TestValue.new(value)
|
expected = TestValue.new(value)
|
||||||
LessThanMatcher.new(expected)
|
LessThanMatcher.new(expected)
|
||||||
end
|
end
|
||||||
|
@ -37,7 +37,7 @@ module Spectator::Matchers
|
||||||
# ```
|
# ```
|
||||||
# expect(0).to be <= 1
|
# expect(0).to be <= 1
|
||||||
# ```
|
# ```
|
||||||
def <=(value : ExpectedType) forall ExpectedType
|
def <=(value)
|
||||||
expected = TestValue.new(value)
|
expected = TestValue.new(value)
|
||||||
LessThanEqualMatcher.new(expected)
|
LessThanEqualMatcher.new(expected)
|
||||||
end
|
end
|
||||||
|
@ -47,7 +47,7 @@ module Spectator::Matchers
|
||||||
# ```
|
# ```
|
||||||
# expect(2).to be > 1
|
# expect(2).to be > 1
|
||||||
# ```
|
# ```
|
||||||
def >(value : ExpectedType) forall ExpectedType
|
def >(value)
|
||||||
expected = TestValue.new(value)
|
expected = TestValue.new(value)
|
||||||
GreaterThanMatcher.new(expected)
|
GreaterThanMatcher.new(expected)
|
||||||
end
|
end
|
||||||
|
@ -57,7 +57,7 @@ module Spectator::Matchers
|
||||||
# ```
|
# ```
|
||||||
# expect(2).to be >= 1
|
# expect(2).to be >= 1
|
||||||
# ```
|
# ```
|
||||||
def >=(value : ExpectedType) forall ExpectedType
|
def >=(value)
|
||||||
expected = TestValue.new(value)
|
expected = TestValue.new(value)
|
||||||
GreaterThanEqualMatcher.new(expected)
|
GreaterThanEqualMatcher.new(expected)
|
||||||
end
|
end
|
||||||
|
@ -67,7 +67,7 @@ module Spectator::Matchers
|
||||||
# ```
|
# ```
|
||||||
# expect(0).to be == 0
|
# expect(0).to be == 0
|
||||||
# ```
|
# ```
|
||||||
def ==(value : ExpectedType) forall ExpectedType
|
def ==(value)
|
||||||
expected = TestValue.new(value)
|
expected = TestValue.new(value)
|
||||||
EqualityMatcher.new(expected)
|
EqualityMatcher.new(expected)
|
||||||
end
|
end
|
||||||
|
@ -77,7 +77,7 @@ module Spectator::Matchers
|
||||||
# ```
|
# ```
|
||||||
# expect(0).to be != 1
|
# expect(0).to be != 1
|
||||||
# ```
|
# ```
|
||||||
def !=(value : ExpectedType) forall ExpectedType
|
def !=(value)
|
||||||
expected = TestValue.new(value)
|
expected = TestValue.new(value)
|
||||||
InequalityMatcher.new(expected)
|
InequalityMatcher.new(expected)
|
||||||
end
|
end
|
||||||
|
@ -114,7 +114,7 @@ module Spectator::Matchers
|
||||||
{
|
{
|
||||||
expected: @truthy ? "Not false or nil" : "false or nil",
|
expected: @truthy ? "Not false or nil" : "false or nil",
|
||||||
actual: actual.value.inspect,
|
actual: actual.value.inspect,
|
||||||
truthy: !!actual.value.inspect,
|
truthy: (!!actual.value).inspect,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -124,7 +124,7 @@ module Spectator::Matchers
|
||||||
{
|
{
|
||||||
expected: @truthy ? "false or nil" : "Not false or nil",
|
expected: @truthy ? "false or nil" : "Not false or nil",
|
||||||
actual: actual.value.inspect,
|
actual: actual.value.inspect,
|
||||||
truthy: !!actual.value.inspect,
|
truthy: (!!actual.value).inspect,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
13
src/spectator/mocks.cr
Normal file
13
src/spectator/mocks.cr
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
require "./mocks/*"
|
||||||
|
|
||||||
|
module Spectator
|
||||||
|
# Functionality for mocking existing types.
|
||||||
|
module Mocks
|
||||||
|
def self.run(context : TestContext)
|
||||||
|
Registry.prepare(context)
|
||||||
|
yield
|
||||||
|
ensure
|
||||||
|
Registry.reset
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
18
src/spectator/mocks/allow.cr
Normal file
18
src/spectator/mocks/allow.cr
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
require "./registry"
|
||||||
|
|
||||||
|
module Spectator::Mocks
|
||||||
|
struct Allow(T)
|
||||||
|
def initialize(@mock : T)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to(stub : MethodStub) : Nil
|
||||||
|
Harness.current.mocks.add_stub(@mock, stub)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to(stubs : Enumerable(MethodStub)) : Nil
|
||||||
|
stubs.each do |stub|
|
||||||
|
Harness.current.mocks.add_stub(@mock, stub)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
15
src/spectator/mocks/allow_any_instance.cr
Normal file
15
src/spectator/mocks/allow_any_instance.cr
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
require "./registry"
|
||||||
|
|
||||||
|
module Spectator::Mocks
|
||||||
|
struct AllowAnyInstance(T)
|
||||||
|
def to(stub : MethodStub) : Nil
|
||||||
|
Harness.current.mocks.add_type_stub(T, stub)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to(stubs : Enumerable(MethodStub)) : Nil
|
||||||
|
stubs.each do |stub|
|
||||||
|
Harness.current.mocks.add_type_stub(T, stub)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
26
src/spectator/mocks/anonymous_double.cr
Normal file
26
src/spectator/mocks/anonymous_double.cr
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
module Spectator::Mocks
|
||||||
|
class AnonymousDouble(T)
|
||||||
|
def initialize(@name : String, @values : T)
|
||||||
|
end
|
||||||
|
|
||||||
|
def as_null_object
|
||||||
|
AnonymousNullDouble.new(@name, @values)
|
||||||
|
end
|
||||||
|
|
||||||
|
macro method_missing(call)
|
||||||
|
args = ::Spectator::Mocks::GenericArguments.create({{call.args.splat}})
|
||||||
|
call = ::Spectator::Mocks::GenericMethodCall.new({{call.name.symbolize}}, args)
|
||||||
|
::Spectator::Harness.current.mocks.record_call(self, call)
|
||||||
|
if (stub = ::Spectator::Harness.current.mocks.find_stub(self, call))
|
||||||
|
stub.call!(args) do
|
||||||
|
@values.fetch({{call.name.symbolize}}) { raise "Consistency error - method stubbed with no implementation"; nil }
|
||||||
|
end
|
||||||
|
else
|
||||||
|
@values.fetch({{call.name.symbolize}}) do
|
||||||
|
return nil if ::Spectator::Harness.current.mocks.expected?(self, call)
|
||||||
|
raise ::Spectator::Mocks::UnexpectedMessageError.new("#{self} received unexpected message {{call.name}}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
17
src/spectator/mocks/anonymous_null_double.cr
Normal file
17
src/spectator/mocks/anonymous_null_double.cr
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
module Spectator::Mocks
|
||||||
|
class AnonymousNullDouble(T)
|
||||||
|
def initialize(@name : String, @values : T)
|
||||||
|
end
|
||||||
|
|
||||||
|
macro method_missing(call)
|
||||||
|
args = ::Spectator::Mocks::GenericArguments.create({{call.args.splat}})
|
||||||
|
call = ::Spectator::Mocks::GenericMethodCall.new({{call.name.symbolize}}, args)
|
||||||
|
::Spectator::Harness.current.mocks.record_call(self, call)
|
||||||
|
if (stub = ::Spectator::Harness.current.mocks.find_stub(self, call))
|
||||||
|
stub.call!(args) { @values.fetch({{call.name.symbolize}}) { self } }
|
||||||
|
else
|
||||||
|
@values.fetch({{call.name.symbolize}}) { self }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
4
src/spectator/mocks/arguments.cr
Normal file
4
src/spectator/mocks/arguments.cr
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
module Spectator::Mocks
|
||||||
|
abstract class Arguments
|
||||||
|
end
|
||||||
|
end
|
98
src/spectator/mocks/double.cr
Normal file
98
src/spectator/mocks/double.cr
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
require "./generic_method_call"
|
||||||
|
require "./generic_method_stub"
|
||||||
|
require "./unexpected_message_error"
|
||||||
|
|
||||||
|
module Spectator::Mocks
|
||||||
|
abstract class Double
|
||||||
|
def initialize(@spectator_double_name : String, @null = false)
|
||||||
|
end
|
||||||
|
|
||||||
|
private macro stub(definition, &block)
|
||||||
|
{%
|
||||||
|
name = nil
|
||||||
|
params = nil
|
||||||
|
args = nil
|
||||||
|
body = nil
|
||||||
|
if definition.is_a?(Call) # stub foo { :bar }
|
||||||
|
named = false
|
||||||
|
name = definition.name.id
|
||||||
|
params = definition.args
|
||||||
|
args = params.map do |p|
|
||||||
|
n = p.is_a?(TypeDeclaration) ? p.var : p.id
|
||||||
|
r = named ? "#{n}: #{n}".id : n
|
||||||
|
named = true if n.starts_with?('*')
|
||||||
|
r
|
||||||
|
end
|
||||||
|
body = definition.block.is_a?(Nop) ? block : definition.block
|
||||||
|
elsif definition.is_a?(TypeDeclaration) # stub foo : Symbol
|
||||||
|
name = definition.var
|
||||||
|
params = [] of MacroId
|
||||||
|
args = [] of MacroId
|
||||||
|
body = block
|
||||||
|
else
|
||||||
|
raise "Unrecognized stub format"
|
||||||
|
end
|
||||||
|
%}
|
||||||
|
|
||||||
|
def {{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %}
|
||||||
|
%args = ::Spectator::Mocks::GenericArguments.create({{args.splat}})
|
||||||
|
%call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args)
|
||||||
|
::Spectator::Harness.current.mocks.record_call(self, %call)
|
||||||
|
if (%stub = ::Spectator::Harness.current.mocks.find_stub(self, %call))
|
||||||
|
%stub.call!(%args) { %method({{args.splat}}) }
|
||||||
|
else
|
||||||
|
%method({{args.splat}})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def {{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %}
|
||||||
|
%args = ::Spectator::Mocks::GenericArguments.create({{args.splat}})
|
||||||
|
%call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args)
|
||||||
|
::Spectator::Harness.current.mocks.record_call(self, %call)
|
||||||
|
if (%stub = ::Spectator::Harness.current.mocks.find_stub(self, %call))
|
||||||
|
%stub.call!(%args) { %method({{args.splat}}) { |*%ya| yield *%ya } }
|
||||||
|
else
|
||||||
|
%method({{args.splat}}) do |*%yield_args|
|
||||||
|
yield *%yield_args
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def %method({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %}
|
||||||
|
{% if body && !body.is_a?(Nop) %}
|
||||||
|
{{body.body}}
|
||||||
|
{% else %}
|
||||||
|
%args = ::Spectator::Mocks::GenericArguments.create({{params.splat}})
|
||||||
|
%call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args)
|
||||||
|
unless ::Spectator::Harness.current.mocks.expected?(self, %call)
|
||||||
|
raise ::Spectator::Mocks::UnexpectedMessageError.new("#{self} received unexpected message {{name}}")
|
||||||
|
end
|
||||||
|
|
||||||
|
# This code shouldn't be reached, but makes the compiler happy to have a matching return type.
|
||||||
|
{% if definition.is_a?(TypeDeclaration) %}
|
||||||
|
%x = uninitialized {{definition.type}}
|
||||||
|
{% else %}
|
||||||
|
nil
|
||||||
|
{% end %}
|
||||||
|
{% end %}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
macro method_missing(call)
|
||||||
|
args = ::Spectator::Mocks::GenericArguments.create({{call.args.splat}})
|
||||||
|
call = ::Spectator::Mocks::GenericMethodCall.new({{call.name.symbolize}}, args)
|
||||||
|
::Spectator::Harness.current.mocks.record_call(self, call)
|
||||||
|
|
||||||
|
return self if @null
|
||||||
|
return self if ::Spectator::Harness.current.mocks.expected?(self, call)
|
||||||
|
|
||||||
|
raise ::Spectator::Mocks::UnexpectedMessageError.new("#{self} received unexpected message {{call.name}}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s(io)
|
||||||
|
io << "Double("
|
||||||
|
io << @spectator_double_name
|
||||||
|
io << ')'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
14
src/spectator/mocks/exception_method_stub.cr
Normal file
14
src/spectator/mocks/exception_method_stub.cr
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
require "./generic_arguments"
|
||||||
|
require "./generic_method_stub"
|
||||||
|
|
||||||
|
module Spectator::Mocks
|
||||||
|
class ExceptionMethodStub(ExceptionType) < GenericMethodStub(Nil)
|
||||||
|
def initialize(name, source, @exception : ExceptionType, args = nil)
|
||||||
|
super(name, source, args)
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(_args : GenericArguments(T, NT), &_original : -> RT) forall T, NT, RT
|
||||||
|
raise @exception
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
23
src/spectator/mocks/expect_any_instance.cr
Normal file
23
src/spectator/mocks/expect_any_instance.cr
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
require "./registry"
|
||||||
|
|
||||||
|
module Spectator::Mocks
|
||||||
|
struct ExpectAnyInstance(T)
|
||||||
|
def initialize(@source : Source)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to(stub : MethodStub) : Nil
|
||||||
|
actual = TestValue.new(T)
|
||||||
|
Harness.current.mocks.expect(T, stub.name)
|
||||||
|
value = TestValue.new(stub.name, stub.to_s)
|
||||||
|
matcher = Matchers::ReceiveTypeMatcher.new(value, stub.arguments?)
|
||||||
|
partial = Expectations::ExpectationPartial.new(actual, @source)
|
||||||
|
partial.to_eventually(matcher)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to(stubs : Enumerable(MethodStub)) : Nil
|
||||||
|
stubs.each do |stub|
|
||||||
|
to(stub)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
42
src/spectator/mocks/generic_arguments.cr
Normal file
42
src/spectator/mocks/generic_arguments.cr
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
require "./arguments"
|
||||||
|
|
||||||
|
module Spectator::Mocks
|
||||||
|
class GenericArguments(T, NT) < Arguments
|
||||||
|
protected getter args
|
||||||
|
protected getter opts
|
||||||
|
|
||||||
|
def initialize(@args : T, @opts : NT)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.create(*args, **opts)
|
||||||
|
GenericArguments.new(args, opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
def pass_to(dispatcher)
|
||||||
|
dispatcher.call(*@args, **@opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
def ===(other) : Bool
|
||||||
|
return false unless @args === other.args
|
||||||
|
return false unless @opts.size === other.opts.size
|
||||||
|
|
||||||
|
@opts.keys.all? do |key|
|
||||||
|
other.opts.has_key?(key) && @opts[key] === other.opts[key]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s(io)
|
||||||
|
@args.each_with_index do |arg, i|
|
||||||
|
arg.inspect(io)
|
||||||
|
io << ", " if i < @args.size - 1
|
||||||
|
end
|
||||||
|
io << ", " unless @args.empty? || @opts.empty?
|
||||||
|
@opts.each_with_index do |key, value, i|
|
||||||
|
io << key
|
||||||
|
io << ": "
|
||||||
|
value.inspect(io)
|
||||||
|
io << ", " if i < @opts.size - 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
19
src/spectator/mocks/generic_method_call.cr
Normal file
19
src/spectator/mocks/generic_method_call.cr
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
require "./generic_arguments"
|
||||||
|
require "./method_call"
|
||||||
|
|
||||||
|
module Spectator::Mocks
|
||||||
|
class GenericMethodCall(T, NT) < MethodCall
|
||||||
|
getter args
|
||||||
|
|
||||||
|
def initialize(name : Symbol, @args : GenericArguments(T, NT))
|
||||||
|
super(name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s(io)
|
||||||
|
super
|
||||||
|
io << '('
|
||||||
|
io << @args
|
||||||
|
io << ')'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
31
src/spectator/mocks/generic_method_stub.cr
Normal file
31
src/spectator/mocks/generic_method_stub.cr
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
require "./arguments"
|
||||||
|
require "./generic_arguments"
|
||||||
|
require "./method_call"
|
||||||
|
require "./method_stub"
|
||||||
|
|
||||||
|
module Spectator::Mocks
|
||||||
|
abstract class GenericMethodStub(ReturnType) < MethodStub
|
||||||
|
getter! arguments : Arguments
|
||||||
|
|
||||||
|
def initialize(name, source, @args : Arguments? = nil)
|
||||||
|
super(name, source)
|
||||||
|
end
|
||||||
|
|
||||||
|
def callable?(call : GenericMethodCall(T, NT)) : Bool forall T, NT
|
||||||
|
super && (!@args || @args === call.args)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s(io)
|
||||||
|
super(io)
|
||||||
|
if @args
|
||||||
|
io << '('
|
||||||
|
io << @args
|
||||||
|
io << ')'
|
||||||
|
end
|
||||||
|
io << " : "
|
||||||
|
io << ReturnType
|
||||||
|
io << " at "
|
||||||
|
io << @source
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
13
src/spectator/mocks/method_call.cr
Normal file
13
src/spectator/mocks/method_call.cr
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
module Spectator::Mocks
|
||||||
|
abstract class MethodCall
|
||||||
|
getter name : Symbol
|
||||||
|
|
||||||
|
def initialize(@name : Symbol)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s(io)
|
||||||
|
io << '#'
|
||||||
|
io << @name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
33
src/spectator/mocks/method_stub.cr
Normal file
33
src/spectator/mocks/method_stub.cr
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
require "../source"
|
||||||
|
require "./generic_method_call"
|
||||||
|
|
||||||
|
module Spectator::Mocks
|
||||||
|
abstract class MethodStub
|
||||||
|
getter name : Symbol
|
||||||
|
|
||||||
|
getter source : Source
|
||||||
|
|
||||||
|
def initialize(@name, @source)
|
||||||
|
end
|
||||||
|
|
||||||
|
def callable?(call : GenericMethodCall(T, NT)) : Bool forall T, NT
|
||||||
|
@name == call.name
|
||||||
|
end
|
||||||
|
|
||||||
|
abstract def call(args : GenericArguments(T, NT), &original : -> RT) forall T, NT, RT
|
||||||
|
|
||||||
|
def call!(args : GenericArguments(T, NT), &original : -> RT) : RT forall T, NT, RT
|
||||||
|
value = call(args, &original)
|
||||||
|
if value.is_a?(RT)
|
||||||
|
value.as(RT)
|
||||||
|
else
|
||||||
|
raise TypeCastError.new("The return type of stub #{self} doesn't match the expected type #{RT}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s(io)
|
||||||
|
io << '#'
|
||||||
|
io << @name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
19
src/spectator/mocks/multi_value_method_stub.cr
Normal file
19
src/spectator/mocks/multi_value_method_stub.cr
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
require "./generic_arguments"
|
||||||
|
require "./generic_method_stub"
|
||||||
|
|
||||||
|
module Spectator::Mocks
|
||||||
|
class MultiValueMethodStub(ReturnType) < GenericMethodStub(ReturnType)
|
||||||
|
@index = 0
|
||||||
|
|
||||||
|
def initialize(name, source, @values : ReturnType, args = nil)
|
||||||
|
super(name, source, args)
|
||||||
|
raise ArgumentError.new("Values must have at least one item") if @values.size < 1
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(_args : GenericArguments(T, NT), &_original : -> RT) forall T, NT, RT
|
||||||
|
value = @values[@index]
|
||||||
|
@index += 1 if @index < @values.size - 1
|
||||||
|
value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
48
src/spectator/mocks/nil_method_stub.cr
Normal file
48
src/spectator/mocks/nil_method_stub.cr
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
require "./generic_arguments"
|
||||||
|
require "./generic_method_stub"
|
||||||
|
require "./value_method_stub"
|
||||||
|
|
||||||
|
module Spectator::Mocks
|
||||||
|
class NilMethodStub < GenericMethodStub(Nil)
|
||||||
|
def call(_args : GenericArguments(T, NT), &_original : -> RT) forall T, NT, RT
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def and_return
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def and_return(value)
|
||||||
|
ValueMethodStub.new(@name, @source, value, @args)
|
||||||
|
end
|
||||||
|
|
||||||
|
def and_return(*values)
|
||||||
|
MultiValueMethodStub.new(@name, @source, values.to_a, @args)
|
||||||
|
end
|
||||||
|
|
||||||
|
def and_raise(exception_type : Exception.class)
|
||||||
|
ExceptionMethodStub.new(@name, @source, exception_type.new, @args)
|
||||||
|
end
|
||||||
|
|
||||||
|
def and_raise(exception : Exception)
|
||||||
|
ExceptionMethodStub.new(@name, @source, exception, @args)
|
||||||
|
end
|
||||||
|
|
||||||
|
def and_raise(message : String)
|
||||||
|
ExceptionMethodStub.new(@name, @source, Exception.new(message), @args)
|
||||||
|
end
|
||||||
|
|
||||||
|
def and_raise(exception_type : Exception.class, *args) forall T
|
||||||
|
ExceptionMethodStub.new(@name, @source, exception_type.new(*args), @args)
|
||||||
|
end
|
||||||
|
|
||||||
|
def with(*args : *T, **opts : **NT) forall T, NT
|
||||||
|
args = GenericArguments.new(args, opts)
|
||||||
|
NilMethodStub.new(@name, @source, args)
|
||||||
|
end
|
||||||
|
|
||||||
|
def and_call_original
|
||||||
|
OriginalMethodStub.new(@name, @source, @args)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
10
src/spectator/mocks/original_method_stub.cr
Normal file
10
src/spectator/mocks/original_method_stub.cr
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
require "./generic_arguments"
|
||||||
|
require "./generic_method_stub"
|
||||||
|
|
||||||
|
module Spectator::Mocks
|
||||||
|
class OriginalMethodStub < GenericMethodStub(Nil)
|
||||||
|
def call(_args : GenericArguments(T, NT), &_original : -> RT) forall T, NT, RT
|
||||||
|
yield
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
18
src/spectator/mocks/proc_method_stub.cr
Normal file
18
src/spectator/mocks/proc_method_stub.cr
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
require "./arguments"
|
||||||
|
require "./generic_method_stub"
|
||||||
|
|
||||||
|
module Spectator::Mocks
|
||||||
|
class ProcMethodStub(ReturnType) < GenericMethodStub(ReturnType)
|
||||||
|
def initialize(name, source, @proc : -> ReturnType, args = nil)
|
||||||
|
super(name, source, args)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.create(name, source, args = nil, &block : -> T) forall T
|
||||||
|
ProcMethodStub.new(name, source, block, args)
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(_args : GenericArguments(T, NT), &_original : -> RT) forall T, NT, RT
|
||||||
|
@proc.call
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
21
src/spectator/mocks/reflection.cr
Normal file
21
src/spectator/mocks/reflection.cr
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
require "../anything"
|
||||||
|
|
||||||
|
module Spectator::Mocks
|
||||||
|
module Reflection
|
||||||
|
private macro _spectator_reflect
|
||||||
|
{% for meth in @type.methods %}
|
||||||
|
%source = ::Spectator::Source.new({{meth.filename}}, {{meth.line_number}})
|
||||||
|
%args = ::Spectator::Mocks::GenericArguments.create(
|
||||||
|
{% for arg, i in meth.args %}
|
||||||
|
{% if meth.splat_index && i == meth.splat_index %}
|
||||||
|
*{{arg.restriction || "::Spectator::Anything.new".id}}{% if i < meth.args.size %},{% end %}
|
||||||
|
{% else %}
|
||||||
|
{{arg.restriction || "::Spectator::Anything.new".id}}{% if i < meth.args.size %},{% end %}
|
||||||
|
{% end %}
|
||||||
|
{% end %}
|
||||||
|
)
|
||||||
|
::Spectator::Mocks::TypeRegistry.add({{@type.id.stringify}}, {{meth.name.symbolize}}, %source, %args)
|
||||||
|
{% end %}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
107
src/spectator/mocks/registry.cr
Normal file
107
src/spectator/mocks/registry.cr
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
module Spectator::Mocks
|
||||||
|
class Registry
|
||||||
|
alias Key = Tuple(String, UInt64)
|
||||||
|
|
||||||
|
private struct Entry
|
||||||
|
getter stubs = Deque(MethodStub).new
|
||||||
|
getter calls = Deque(MethodCall).new
|
||||||
|
getter expected = Set(MethodStub).new
|
||||||
|
end
|
||||||
|
|
||||||
|
@all_instances = {} of String => Entry
|
||||||
|
@entries = {} of Key => Entry
|
||||||
|
|
||||||
|
def initialize(context : TestContext)
|
||||||
|
current_context = context
|
||||||
|
while current_context
|
||||||
|
current_context.stubs.each do |k, v|
|
||||||
|
stubs = if @all_instances.has_key?(k)
|
||||||
|
@all_instances[k].stubs
|
||||||
|
else
|
||||||
|
entry = Entry.new
|
||||||
|
@all_instances[k] = entry
|
||||||
|
entry.stubs
|
||||||
|
end
|
||||||
|
stubs.concat(v)
|
||||||
|
end
|
||||||
|
current_context = current_context.parent?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def reset : Nil
|
||||||
|
@entries.clear
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_stub(object, stub : MethodStub) : Nil
|
||||||
|
# Stubs are added in reverse order,
|
||||||
|
# so that later-defined stubs override previously defined ones.
|
||||||
|
fetch_instance(object).stubs.unshift(stub)
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_type_stub(type, stub : MethodStub) : Nil
|
||||||
|
fetch_type(type).stubs.unshift(stub)
|
||||||
|
end
|
||||||
|
|
||||||
|
def stubbed?(object, method_name : Symbol) : Bool
|
||||||
|
fetch_instance(object).stubs.any? { |stub| stub.name == method_name } ||
|
||||||
|
fetch_type(object.class).stubs.any? { |stub| stub.name == method_name }
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_stub(object, call : GenericMethodCall(T, NT)) forall T, NT
|
||||||
|
fetch_instance(object).stubs.find(&.callable?(call)) ||
|
||||||
|
fetch_type(object.class).stubs.find(&.callable?(call))
|
||||||
|
end
|
||||||
|
|
||||||
|
def record_call(object, call : MethodCall) : Nil
|
||||||
|
fetch_instance(object).calls << call
|
||||||
|
fetch_type(object.class).calls << call
|
||||||
|
end
|
||||||
|
|
||||||
|
def calls_for(object, method_name : Symbol)
|
||||||
|
fetch_instance(object).calls.select { |call| call.name == method_name }
|
||||||
|
end
|
||||||
|
|
||||||
|
def calls_for_type(type : T.class, method_name : Symbol) forall T
|
||||||
|
fetch_type(type).calls.select { |call| call.name == method_name }
|
||||||
|
end
|
||||||
|
|
||||||
|
def expected?(object, call : GenericMethodCall(T, NT)) : Bool forall T, NT
|
||||||
|
fetch_instance(object).expected.any?(&.callable?(call)) ||
|
||||||
|
fetch_type(object.class).expected.any?(&.callable?(call))
|
||||||
|
end
|
||||||
|
|
||||||
|
def expect(object, stub : MethodStub) : Nil
|
||||||
|
fetch_instance(object).expected.add(stub)
|
||||||
|
end
|
||||||
|
|
||||||
|
def expect(type : T.class, stub : MethodStub) : Nil forall T
|
||||||
|
fetch_type(type).expected.add(stub)
|
||||||
|
end
|
||||||
|
|
||||||
|
private def fetch_instance(object)
|
||||||
|
key = unique_key(object)
|
||||||
|
if @entries.has_key?(key)
|
||||||
|
@entries[key]
|
||||||
|
else
|
||||||
|
@entries[key] = Entry.new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private def fetch_type(type)
|
||||||
|
key = type.name
|
||||||
|
if @all_instances.has_key?(key)
|
||||||
|
@all_instances[key]
|
||||||
|
else
|
||||||
|
@all_instances[key] = Entry.new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private def unique_key(reference : Reference)
|
||||||
|
{reference.class.name, reference.object_id}
|
||||||
|
end
|
||||||
|
|
||||||
|
private def unique_key(value : Value)
|
||||||
|
{value.class.name, value.hash}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
80
src/spectator/mocks/stubs.cr
Normal file
80
src/spectator/mocks/stubs.cr
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
module Spectator::Mocks
|
||||||
|
module Stubs
|
||||||
|
private macro stub(definition, _file = __FILE__, _line = __LINE__, &block)
|
||||||
|
{%
|
||||||
|
receiver = nil
|
||||||
|
name = nil
|
||||||
|
params = nil
|
||||||
|
args = nil
|
||||||
|
body = nil
|
||||||
|
if definition.is_a?(Call) # stub foo { :bar }
|
||||||
|
receiver = definition.receiver.id
|
||||||
|
named = false
|
||||||
|
name = definition.name.id
|
||||||
|
params = definition.args
|
||||||
|
args = params.map do |p|
|
||||||
|
n = p.is_a?(TypeDeclaration) ? p.var : p.id
|
||||||
|
r = named ? "#{n}: #{n}".id : n
|
||||||
|
named = true if n.starts_with?('*')
|
||||||
|
r
|
||||||
|
end
|
||||||
|
body = definition.block.is_a?(Nop) ? block : definition.block
|
||||||
|
elsif definition.is_a?(TypeDeclaration) # stub foo : Symbol
|
||||||
|
name = definition.var
|
||||||
|
params = [] of MacroId
|
||||||
|
args = [] of MacroId
|
||||||
|
body = block
|
||||||
|
else
|
||||||
|
raise "Unrecognized stub format"
|
||||||
|
end
|
||||||
|
|
||||||
|
original = if @type.methods.find { |m| m.name.id == name }
|
||||||
|
:previous_def
|
||||||
|
else
|
||||||
|
:super
|
||||||
|
end.id
|
||||||
|
receiver = if receiver == :self.id
|
||||||
|
original = :previous_def.id
|
||||||
|
"self."
|
||||||
|
else
|
||||||
|
""
|
||||||
|
end.id
|
||||||
|
%}
|
||||||
|
|
||||||
|
{% if body && !body.is_a?(Nop) %}
|
||||||
|
%source = ::Spectator::Source.new({{_file}}, {{_line}})
|
||||||
|
%proc = ->{
|
||||||
|
{{body.body}}
|
||||||
|
}
|
||||||
|
%ds = ::Spectator::Mocks::ProcMethodStub.new({{name.symbolize}}, %source, %proc)
|
||||||
|
::Spectator::SpecBuilder.add_default_stub({{@type.name}}, %ds)
|
||||||
|
{% end %}
|
||||||
|
|
||||||
|
def {{receiver}}{{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %}
|
||||||
|
if (%harness = ::Spectator::Harness.current?)
|
||||||
|
%args = ::Spectator::Mocks::GenericArguments.create({{args.splat}})
|
||||||
|
%call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args)
|
||||||
|
%harness.mocks.record_call(self, %call)
|
||||||
|
if (%stub = %harness.mocks.find_stub(self, %call))
|
||||||
|
return %stub.call!(%args) { {{original}}({{args.splat}}) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
{{original}}({{args.splat}})
|
||||||
|
end
|
||||||
|
|
||||||
|
def {{receiver}}{{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %}
|
||||||
|
if (%harness = ::Spectator::Harness.current?)
|
||||||
|
%args = ::Spectator::Mocks::GenericArguments.create({{args.splat}})
|
||||||
|
%call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args)
|
||||||
|
%harness.mocks.record_call(self, %call)
|
||||||
|
if (%stub = %harness.mocks.find_stub(self, %call))
|
||||||
|
return %stub.call!(%args) { {{original}}({{args.splat}}) { |*%ya| yield *%ya } }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
{{original}}({{args.splat}}) do |*%yield_args|
|
||||||
|
yield *%yield_args
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
25
src/spectator/mocks/type_registry.cr
Normal file
25
src/spectator/mocks/type_registry.cr
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
module Spectator::Mocks
|
||||||
|
module TypeRegistry
|
||||||
|
extend self
|
||||||
|
|
||||||
|
alias Key = Tuple(String, Symbol)
|
||||||
|
|
||||||
|
@@entries = {} of Key => Deque(MethodStub)
|
||||||
|
|
||||||
|
def add(type_name : String, method_name : Symbol, source : Source, args : Arguments) : Nil
|
||||||
|
key = {type_name, method_name}
|
||||||
|
list = if @@entries.has_key?(key)
|
||||||
|
@@entries[key]
|
||||||
|
else
|
||||||
|
@@entries[key] = Deque(MethodStub).new
|
||||||
|
end
|
||||||
|
list << NilMethodStub.new(method_name, source, args)
|
||||||
|
end
|
||||||
|
|
||||||
|
def exists?(type_name : String, call : GenericMethodCall(T, NT)) : Bool forall T, NT
|
||||||
|
key = {type_name, call.name}
|
||||||
|
list = @@entries.fetch(key) { return false }
|
||||||
|
list.any?(&.callable?(call))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
4
src/spectator/mocks/unexpected_message_error.cr
Normal file
4
src/spectator/mocks/unexpected_message_error.cr
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
module Spectator::Mocks
|
||||||
|
class UnexpectedMessageError < Exception
|
||||||
|
end
|
||||||
|
end
|
14
src/spectator/mocks/value_method_stub.cr
Normal file
14
src/spectator/mocks/value_method_stub.cr
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
require "./generic_arguments"
|
||||||
|
require "./generic_method_stub"
|
||||||
|
|
||||||
|
module Spectator::Mocks
|
||||||
|
class ValueMethodStub(ReturnType) < GenericMethodStub(ReturnType)
|
||||||
|
def initialize(name, source, @value : ReturnType, args = nil)
|
||||||
|
super(name, source, args)
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(_args : GenericArguments(T, NT), &_original : -> RT) forall T, NT, RT
|
||||||
|
@value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
108
src/spectator/mocks/verifying_double.cr
Normal file
108
src/spectator/mocks/verifying_double.cr
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
module Spectator::Mocks
|
||||||
|
abstract class VerifyingDouble(T)
|
||||||
|
def initialize(@null = false)
|
||||||
|
end
|
||||||
|
|
||||||
|
private macro stub(definition, &block)
|
||||||
|
{%
|
||||||
|
name = nil
|
||||||
|
params = nil
|
||||||
|
args = nil
|
||||||
|
body = nil
|
||||||
|
if definition.is_a?(Call) # stub foo { :bar }
|
||||||
|
named = false
|
||||||
|
name = definition.name.id
|
||||||
|
params = definition.args
|
||||||
|
args = params.map do |p|
|
||||||
|
n = p.is_a?(TypeDeclaration) ? p.var : p.id
|
||||||
|
r = named ? "#{n}: #{n}".id : n
|
||||||
|
named = true if n.starts_with?('*')
|
||||||
|
r
|
||||||
|
end
|
||||||
|
body = definition.block.is_a?(Nop) ? block : definition.block
|
||||||
|
elsif definition.is_a?(TypeDeclaration) # stub foo : Symbol
|
||||||
|
name = definition.var
|
||||||
|
params = [] of MacroId
|
||||||
|
args = [] of MacroId
|
||||||
|
body = block
|
||||||
|
else
|
||||||
|
raise "Unrecognized stub format"
|
||||||
|
end
|
||||||
|
%}
|
||||||
|
|
||||||
|
def {{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %}
|
||||||
|
%args = ::Spectator::Mocks::GenericArguments.create({{args.splat}})
|
||||||
|
%call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args)
|
||||||
|
::Spectator::Harness.current.mocks.record_call(self, %call)
|
||||||
|
|
||||||
|
unless ::Spectator::Mocks::TypeRegistry.exists?(T.to_s, %call)
|
||||||
|
raise ::Spectator::Mocks::UnexpectedMessageError.new("#{self} received unexpected message {{name}} - #{T} does not respond to #{%call}")
|
||||||
|
end
|
||||||
|
|
||||||
|
if (%stub = ::Spectator::Harness.current.mocks.find_stub(self, %call))
|
||||||
|
%stub.call!(%args) { %method({{args.splat}}) }
|
||||||
|
else
|
||||||
|
%method({{args.splat}})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def {{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %}
|
||||||
|
%args = ::Spectator::Mocks::GenericArguments.create({{args.splat}})
|
||||||
|
%call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args)
|
||||||
|
::Spectator::Harness.current.mocks.record_call(self, %call)
|
||||||
|
|
||||||
|
unless ::Spectator::Mocks::TypeRegistry.exists?(T.to_s, %call)
|
||||||
|
raise ::Spectator::Mocks::UnexpectedMessageError.new("#{self} received unexpected message {{name}} - #{T} does not respond to #{%call}")
|
||||||
|
end
|
||||||
|
|
||||||
|
if (%stub = ::Spectator::Harness.current.mocks.find_stub(self, %call))
|
||||||
|
%stub.call!(%args) { %method({{args.splat}}) { |*%ya| yield *%ya } }
|
||||||
|
else
|
||||||
|
%method({{args.splat}}) do |*%yield_args|
|
||||||
|
yield *%yield_args
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def %method({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %}
|
||||||
|
{% if body && !body.is_a?(Nop) %}
|
||||||
|
{{body.body}}
|
||||||
|
{% else %}
|
||||||
|
%args = ::Spectator::Mocks::GenericArguments.create({{params.splat}})
|
||||||
|
%call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args)
|
||||||
|
unless ::Spectator::Harness.current.mocks.expected?(self, %call)
|
||||||
|
raise ::Spectator::Mocks::UnexpectedMessageError.new("#{self} received unexpected message {{name}}")
|
||||||
|
end
|
||||||
|
|
||||||
|
# This code shouldn't be reached, but makes the compiler happy to have a matching return type.
|
||||||
|
{% if definition.is_a?(TypeDeclaration) %}
|
||||||
|
%x = uninitialized {{definition.type}}
|
||||||
|
{% else %}
|
||||||
|
nil
|
||||||
|
{% end %}
|
||||||
|
{% end %}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
macro method_missing(call)
|
||||||
|
args = ::Spectator::Mocks::GenericArguments.create({{call.args.splat}})
|
||||||
|
call = ::Spectator::Mocks::GenericMethodCall.new({{call.name.symbolize}}, args)
|
||||||
|
::Spectator::Harness.current.mocks.record_call(self, call)
|
||||||
|
|
||||||
|
unless ::Spectator::Mocks::TypeRegistry.exists?(T.to_s, call)
|
||||||
|
raise ::Spectator::Mocks::UnexpectedMessageError.new("#{self} received unexpected message {{call.name}} - #{T} does not respond to #{call}")
|
||||||
|
end
|
||||||
|
|
||||||
|
return self if @null
|
||||||
|
return self if ::Spectator::Harness.current.mocks.expected?(self, call)
|
||||||
|
|
||||||
|
raise ::Spectator::Mocks::UnexpectedMessageError.new("#{self} received unexpected message {{call.name}}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s(io)
|
||||||
|
io << "Double("
|
||||||
|
io << T
|
||||||
|
io << ')'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -20,7 +20,7 @@ module Spectator
|
||||||
context.run_before_hooks(self)
|
context.run_before_hooks(self)
|
||||||
run_example(result)
|
run_example(result)
|
||||||
context.run_after_hooks(self)
|
context.run_after_hooks(self)
|
||||||
run_deferred(result)
|
run_deferred(result) unless result.error
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -96,6 +96,10 @@ module Spectator
|
||||||
@@stack.current.add_post_condition(block)
|
@@stack.current.add_post_condition(block)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def add_default_stub(*args) : Nil
|
||||||
|
@@stack.current.add_default_stub(*args)
|
||||||
|
end
|
||||||
|
|
||||||
# Builds the entire spec and returns it as a test suite.
|
# Builds the entire spec and returns it as a test suite.
|
||||||
# This should be called only once after the entire spec has been defined.
|
# This should be called only once after the entire spec has been defined.
|
||||||
protected def build(filter : ExampleFilter) : TestSuite
|
protected def build(filter : ExampleFilter) : TestSuite
|
||||||
|
|
|
@ -14,6 +14,7 @@ module Spectator::SpecBuilder
|
||||||
@around_each_hooks = Deque(::SpectatorTest, Proc(Nil) ->).new
|
@around_each_hooks = Deque(::SpectatorTest, Proc(Nil) ->).new
|
||||||
@pre_conditions = Deque(TestMetaMethod).new
|
@pre_conditions = Deque(TestMetaMethod).new
|
||||||
@post_conditions = Deque(TestMetaMethod).new
|
@post_conditions = Deque(TestMetaMethod).new
|
||||||
|
@default_stubs = {} of String => Deque(Mocks::MethodStub)
|
||||||
|
|
||||||
def add_child(child : Child)
|
def add_child(child : Child)
|
||||||
@children << child
|
@children << child
|
||||||
|
@ -47,6 +48,12 @@ module Spectator::SpecBuilder
|
||||||
@post_conditions << hook
|
@post_conditions << hook
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def add_default_stub(type : T.class, stub : Mocks::MethodStub) forall T
|
||||||
|
key = type.name
|
||||||
|
@default_stubs[key] = Deque(Mocks::MethodStub).new unless @default_stubs.has_key?(key)
|
||||||
|
@default_stubs[key].unshift(stub)
|
||||||
|
end
|
||||||
|
|
||||||
private def build_hooks
|
private def build_hooks
|
||||||
ExampleHooks.new(
|
ExampleHooks.new(
|
||||||
@before_all_hooks.to_a,
|
@before_all_hooks.to_a,
|
||||||
|
|
|
@ -7,7 +7,7 @@ module Spectator::SpecBuilder
|
||||||
end
|
end
|
||||||
|
|
||||||
def build(parent_group)
|
def build(parent_group)
|
||||||
context = TestContext.new(parent_group.context, build_hooks, build_conditions, parent_group.context.values)
|
context = TestContext.new(parent_group.context, build_hooks, build_conditions, parent_group.context.values, @default_stubs)
|
||||||
NestedExampleGroup.new(@description, @source, parent_group, context).tap do |group|
|
NestedExampleGroup.new(@description, @source, parent_group, context).tap do |group|
|
||||||
group.children = children.map do |child|
|
group.children = children.map do |child|
|
||||||
child.build(group).as(ExampleComponent)
|
child.build(group).as(ExampleComponent)
|
||||||
|
|
|
@ -4,7 +4,7 @@ require "./example_group_builder"
|
||||||
module Spectator::SpecBuilder
|
module Spectator::SpecBuilder
|
||||||
class RootExampleGroupBuilder < ExampleGroupBuilder
|
class RootExampleGroupBuilder < ExampleGroupBuilder
|
||||||
def build
|
def build
|
||||||
context = TestContext.new(nil, build_hooks, build_conditions, TestValues.empty)
|
context = TestContext.new(nil, build_hooks, build_conditions, TestValues.empty, {} of String => Deque(Mocks::MethodStub))
|
||||||
RootExampleGroup.new(context).tap do |group|
|
RootExampleGroup.new(context).tap do |group|
|
||||||
group.children = children.map do |child|
|
group.children = children.map do |child|
|
||||||
child.build(group).as(ExampleComponent)
|
child.build(group).as(ExampleComponent)
|
||||||
|
|
|
@ -9,7 +9,7 @@ module Spectator::SpecBuilder
|
||||||
def build(parent_group)
|
def build(parent_group)
|
||||||
values = parent_group.context.values
|
values = parent_group.context.values
|
||||||
collection = @collection_builder.call(values)
|
collection = @collection_builder.call(values)
|
||||||
context = TestContext.new(parent_group.context, build_hooks, build_conditions, values)
|
context = TestContext.new(parent_group.context, build_hooks, build_conditions, values, @default_stubs)
|
||||||
NestedExampleGroup.new(@description, @source, parent_group, context).tap do |group|
|
NestedExampleGroup.new(@description, @source, parent_group, context).tap do |group|
|
||||||
group.children = collection.map do |element|
|
group.children = collection.map do |element|
|
||||||
build_sub_group(group, element).as(ExampleComponent)
|
build_sub_group(group, element).as(ExampleComponent)
|
||||||
|
@ -19,7 +19,7 @@ module Spectator::SpecBuilder
|
||||||
|
|
||||||
private def build_sub_group(parent_group, element)
|
private def build_sub_group(parent_group, element)
|
||||||
values = parent_group.context.values.add(@id, @description.to_s, element)
|
values = parent_group.context.values.add(@id, @description.to_s, element)
|
||||||
context = TestContext.new(parent_group.context, ExampleHooks.empty, ExampleConditions.empty, values)
|
context = TestContext.new(parent_group.context, ExampleHooks.empty, ExampleConditions.empty, values, {} of String => Deque(Mocks::MethodStub))
|
||||||
NestedExampleGroup.new("#{@label} = #{element.inspect}", @source, parent_group, context).tap do |group|
|
NestedExampleGroup.new("#{@label} = #{element.inspect}", @source, parent_group, context).tap do |group|
|
||||||
group.children = children.map do |child|
|
group.children = children.map do |child|
|
||||||
child.build(group).as(ExampleComponent)
|
child.build(group).as(ExampleComponent)
|
||||||
|
|
|
@ -3,9 +3,17 @@ require "./test_values"
|
||||||
|
|
||||||
module Spectator
|
module Spectator
|
||||||
class TestContext
|
class TestContext
|
||||||
|
getter! parent
|
||||||
|
|
||||||
getter values
|
getter values
|
||||||
|
|
||||||
def initialize(@parent : TestContext?, @hooks : ExampleHooks, @conditions : ExampleConditions, @values : TestValues)
|
getter stubs : Hash(String, Deque(Mocks::MethodStub))
|
||||||
|
|
||||||
|
def initialize(@parent : TestContext?,
|
||||||
|
@hooks : ExampleHooks,
|
||||||
|
@conditions : ExampleConditions,
|
||||||
|
@values : TestValues,
|
||||||
|
@stubs : Hash(String, Deque(Mocks::MethodStub)))
|
||||||
@before_all_hooks_run = false
|
@before_all_hooks_run = false
|
||||||
@after_all_hooks_run = false
|
@after_all_hooks_run = false
|
||||||
end
|
end
|
||||||
|
|
|
@ -18,8 +18,8 @@ module Spectator
|
||||||
# Adds a new value by duplicating the current set and adding to it.
|
# Adds a new value by duplicating the current set and adding to it.
|
||||||
# The new sample values with the additional value is returned.
|
# The new sample values with the additional value is returned.
|
||||||
# The original set of sample values is not modified.
|
# The original set of sample values is not modified.
|
||||||
def add(id : Symbol, name : String, value : T) : TestValues forall T
|
def add(id : Symbol, name : String, value) : TestValues
|
||||||
wrapper = TypedValueWrapper(T).new(value)
|
wrapper = TypedValueWrapper.new(value)
|
||||||
TestValues.new(@values.merge({
|
TestValues.new(@values.merge({
|
||||||
id => Entry.new(name, wrapper),
|
id => Entry.new(name, wrapper),
|
||||||
}))
|
}))
|
||||||
|
|
Loading…
Reference in a new issue