diff --git a/spec/spectator/mocks/mock_spec.cr b/spec/spectator/mocks/mock_spec.cr new file mode 100644 index 0000000..6ca3dda --- /dev/null +++ b/spec/spectator/mocks/mock_spec.cr @@ -0,0 +1,44 @@ +require "../../spec_helper" + +Spectator.describe Spectator::Mock do + describe "#define_subclass" do + class Thing + def method1 + 42 + end + + def method2 + :original + end + + def method3 + "original" + end + end + + Spectator::Mock.define_subclass(Thing, MockThing, :mock_name, method1: 123) do + stub def method2 + :stubbed + end + end + + let(thing) { MockThing.new } + + it "defines a subclass of the mocked type" do + expect(MockThing).to be_lt(Thing) + end + + it "overrides responses from methods with keyword arguments" do + expect(thing.method1).to eq(123) + end + + it "overrides responses from methods defined in the block" do + expect(thing.method2).to eq(:stubbed) + end + + it "allows methods to be stubbed" do + stub = Spectator::ValueStub.new(:method3, "stubbed") + expect { thing._spectator_define_stub(stub) }.to change { thing.method3 }.from("original").to("stubbed") + end + end +end diff --git a/src/spectator/mocks/mock.cr b/src/spectator/mocks/mock.cr new file mode 100644 index 0000000..f4c2f16 --- /dev/null +++ b/src/spectator/mocks/mock.cr @@ -0,0 +1,47 @@ +require "./method_call" +require "./mocked" +require "./stub" +require "./stubbed_name" +require "./value_stub" + +module Spectator + module Mock + macro define_subclass(mocked_type, type_name, name = nil, **value_methods, &block) + {% if name %}@[::Spectator::StubbedName({{name}})]{% end %} + class {{type_name.id}} < {{mocked_type.id}} + include ::Spectator::Mocked + + {% begin %} + @_spectator_stubs = [ + {% for key, value in value_methods %} + ::Spectator::ValueStub.new({{key.id.symbolize}}, {{value}}), + {% end %} + ] of ::Spectator::Stub + {% end %} + + def _spectator_define_stub(stub : ::Spectator::Stub) : Nil + @_spectator_stubs.unshift(stub) + end + + def _spectator_find_stub(call : ::Spectator::MethodCall) : ::Spectator::Stub? + @_spectator_stubs.find &.===(call) + end + + # Returns the mock's name formatted for user output. + private def _spectator_stubbed_name : String + \{% if anno = @type.annotation(::Spectator::StubbedName) %} + "#" + \{% else %} + "#" + \{% end %} + end + + macro finished + stub_all {{mocked_type.id}} + end + + {% if block %}{{block.body}}{% end %} + end + end + end +end diff --git a/src/spectator/mocks/mocked.cr b/src/spectator/mocks/mocked.cr new file mode 100644 index 0000000..c7f9205 --- /dev/null +++ b/src/spectator/mocks/mocked.cr @@ -0,0 +1,52 @@ +require "./method_call" +require "./stubbable" + +module Spectator + module Mocked + include Stubbable + + # Method called when a stub isn't found. + # + # The received message is captured in *call*. + # Yield to call the original method's implementation. + # The stubbed method returns the value returned by this method. + # This method can also raise an error if it's impossible to return something. + def _spectator_stub_fallback(call : MethodCall, &) + raise "oof" + end + + # Method called when a stub isn't found. + # + # The received message is captured in *call*. + # The expected return type is provided by *type*. + # Yield to call the original method's implementation. + # The stubbed method returns the value returned by this method. + # This method can also raise an error if it's impossible to return something. + def _spectator_stub_fallback(call : MethodCall, type, &) + raise "oof" + end + + # Method called when a stub isn't found. + # + # This is similar to `#_spectator_stub_fallback`, + # but called when the original (un-stubbed) method isn't available. + # The received message is captured in *call*. + # The stubbed method returns the value returned by this method. + # This method can also raise an error if it's impossible to return something. + def _spectator_abstract_stub_fallback(call : MethodCall) + raise "oof" + end + + # Method called when a stub isn't found. + # + # This is similar to `#_spectator_stub_fallback`, + # but called when the original (un-stubbed) method isn't available. + # The received message is captured in *call*. + # The expected return type is provided by *type*. + # The stubbed method returns the value returned by this method. + # This method can also raise an error if it's impossible to return something. + def _spectator_abstract_stub_fallback(call : MethodCall, type) + raise "oof" + end + end +end