diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index e155960..fca156d 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -121,6 +121,11 @@ module Spectator::DSL 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__) %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) ::Spectator::Mocks::NilMethodStub.new({{method_name.id.symbolize}}, %source) diff --git a/src/spectator/matchers/receive_type_matcher.cr b/src/spectator/matchers/receive_type_matcher.cr new file mode 100644 index 0000000..b5ffce4 --- /dev/null +++ b/src/spectator/matchers/receive_type_matcher.cr @@ -0,0 +1,111 @@ +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 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 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, (count..count)) + end + + def at_least(count) + Count.new(@expected, (count..)) + end + + def at_most(count) + Count.new(@expected, (..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 diff --git a/src/spectator/mocks/expect_any_instance.cr b/src/spectator/mocks/expect_any_instance.cr new file mode 100644 index 0000000..c51c0e8 --- /dev/null +++ b/src/spectator/mocks/expect_any_instance.cr @@ -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 diff --git a/src/spectator/mocks/registry.cr b/src/spectator/mocks/registry.cr index d094052..e01d186 100644 --- a/src/spectator/mocks/registry.cr +++ b/src/spectator/mocks/registry.cr @@ -61,18 +61,23 @@ module Spectator::Mocks fetch_instance(object).calls.select { |call| call.name == method_name } end - def calls_for_type(type, method_name : Symbol) + 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, method_name : Symbol) : Bool - fetch_instance(object).expected.includes?(method_name) + fetch_instance(object).expected.includes?(method_name) || + fetch_type(object.class).expected.includes?(method_name) end def expect(object, method_name : Symbol) : Nil fetch_instance(object).expected.add(method_name) end + def expect(type : T.class, method_name : Symbol) : Nil forall T + fetch_type(type).expected.add(method_name) + end + private def fetch_instance(object) key = unique_key(object) if @entries.has_key?(key)