From f7147299ab4d3106a1f14b93dac6761b30c03eae Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 4 Jul 2022 20:19:13 -0600 Subject: [PATCH] Add stub support to class methods on mocks --- spec/spectator/mocks/mock_spec.cr | 210 ++++++++++++++++++++++++++++++ src/spectator/mocks/mock.cr | 11 ++ 2 files changed, 221 insertions(+) diff --git a/spec/spectator/mocks/mock_spec.cr b/spec/spectator/mocks/mock_spec.cr index 3eb78db..dd9f16a 100644 --- a/spec/spectator/mocks/mock_spec.cr +++ b/spec/spectator/mocks/mock_spec.cr @@ -287,6 +287,111 @@ Spectator.describe Spectator::Mock do expect(mock._spectator_invocations).to contain_exactly(:method3) end end + + context "class method stubs" do + class Thing + def self.foo + :original + end + + def self.bar(arg) + arg + end + + def self.baz(arg) + yield + end + end + + Spectator::Mock.define_subtype(:class, Thing, MockThing) do + stub def self.foo + :stub + end + end + + let(mock) { MockThing } + let(foo_stub) { Spectator::ValueStub.new(:foo, :override) } + + after_each { mock._spectator_clear_stubs } + + it "overrides an existing method" do + expect { mock._spectator_define_stub(foo_stub) }.to change { mock.foo }.from(:stub).to(:override) + end + + it "doesn't affect other methods" do + expect { mock._spectator_define_stub(foo_stub) }.to_not change { mock.bar(42) } + end + + it "replaces an existing stub" do + mock._spectator_define_stub(foo_stub) + stub = Spectator::ValueStub.new(:foo, :replacement) + expect { mock._spectator_define_stub(stub) }.to change { mock.foo }.to(:replacement) + end + + it "picks the correct stub based on arguments" do + stub1 = Spectator::ValueStub.new(:bar, :fallback) + stub2 = Spectator::ValueStub.new(:bar, :override, Spectator::Arguments.capture(:match)) + mock._spectator_define_stub(stub1) + mock._spectator_define_stub(stub2) + aggregate_failures do + expect(mock.bar(:wrong)).to eq(:fallback) + expect(mock.bar(:match)).to eq(:override) + end + end + + it "only uses a stub if an argument constraint is met" do + stub = Spectator::ValueStub.new(:bar, :override, Spectator::Arguments.capture(:match)) + mock._spectator_define_stub(stub) + aggregate_failures do + expect(mock.bar(:original)).to eq(:original) + expect(mock.bar(:match)).to eq(:override) + end + end + + it "ignores the block argument if not in the constraint" do + stub1 = Spectator::ValueStub.new(:baz, 1) + stub2 = Spectator::ValueStub.new(:baz, 2, Spectator::Arguments.capture(3)) + mock._spectator_define_stub(stub1) + mock._spectator_define_stub(stub2) + aggregate_failures do + expect(mock.baz(5) { 42 }).to eq(1) + expect(mock.baz(3) { 42 }).to eq(2) + end + end + + describe "._spectator_clear_stubs" do + before_each { mock._spectator_define_stub(foo_stub) } + + it "removes previously defined stubs" do + expect { mock._spectator_clear_stubs }.to change { mock.foo }.from(:override).to(:stub) + end + end + + describe "._spectator_calls" do + before_each { mock._spectator_clear_calls } + + # Retrieves symbolic names of methods called on a mock. + def called_method_names(mock) + mock._spectator_calls.map(&.method) + end + + it "stores calls to stubbed methods" do + expect { mock.foo }.to change { called_method_names(mock) }.from(%i[]).to(%i[foo]) + end + + it "stores multiple calls to the same stub" do + mock.foo + expect { mock.foo }.to change { called_method_names(mock) }.from(%i[foo]).to(%i[foo foo]) + end + + it "stores arguments for a call" do + mock.bar(42) + args = Spectator::Arguments.capture(42) + call = mock._spectator_calls.first + expect(call.arguments).to eq(args) + end + end + end end describe "#inject" do @@ -493,5 +598,110 @@ Spectator.describe Spectator::Mock do expect(MockedStruct._spectator_invocations).to contain_exactly(:method3) end end + + context "class method stubs" do + class Thing + def self.foo + :original + end + + def self.bar(arg) + arg + end + + def self.baz(arg) + yield + end + end + + Spectator::Mock.inject(:class, Thing) do + stub def self.foo + :stub + end + end + + let(mock) { Thing } + let(foo_stub) { Spectator::ValueStub.new(:foo, :override) } + + after_each { mock._spectator_clear_stubs } + + it "overrides an existing method" do + expect { mock._spectator_define_stub(foo_stub) }.to change { mock.foo }.from(:stub).to(:override) + end + + it "doesn't affect other methods" do + expect { mock._spectator_define_stub(foo_stub) }.to_not change { mock.bar(42) } + end + + it "replaces an existing stub" do + mock._spectator_define_stub(foo_stub) + stub = Spectator::ValueStub.new(:foo, :replacement) + expect { mock._spectator_define_stub(stub) }.to change { mock.foo }.to(:replacement) + end + + it "picks the correct stub based on arguments" do + stub1 = Spectator::ValueStub.new(:bar, :fallback) + stub2 = Spectator::ValueStub.new(:bar, :override, Spectator::Arguments.capture(:match)) + mock._spectator_define_stub(stub1) + mock._spectator_define_stub(stub2) + aggregate_failures do + expect(mock.bar(:wrong)).to eq(:fallback) + expect(mock.bar(:match)).to eq(:override) + end + end + + it "only uses a stub if an argument constraint is met" do + stub = Spectator::ValueStub.new(:bar, :override, Spectator::Arguments.capture(:match)) + mock._spectator_define_stub(stub) + aggregate_failures do + expect(mock.bar(:original)).to eq(:original) + expect(mock.bar(:match)).to eq(:override) + end + end + + it "ignores the block argument if not in the constraint" do + stub1 = Spectator::ValueStub.new(:baz, 1) + stub2 = Spectator::ValueStub.new(:baz, 2, Spectator::Arguments.capture(3)) + mock._spectator_define_stub(stub1) + mock._spectator_define_stub(stub2) + aggregate_failures do + expect(mock.baz(5) { 42 }).to eq(1) + expect(mock.baz(3) { 42 }).to eq(2) + end + end + + describe "._spectator_clear_stubs" do + before_each { mock._spectator_define_stub(foo_stub) } + + it "removes previously defined stubs" do + expect { mock._spectator_clear_stubs }.to change { mock.foo }.from(:override).to(:stub) + end + end + + describe "._spectator_calls" do + before_each { mock._spectator_clear_calls } + + # Retrieves symbolic names of methods called on a mock. + def called_method_names(mock) + mock._spectator_calls.map(&.method) + end + + it "stores calls to stubbed methods" do + expect { mock.foo }.to change { called_method_names(mock) }.from(%i[]).to(%i[foo]) + end + + it "stores multiple calls to the same stub" do + mock.foo + expect { mock.foo }.to change { called_method_names(mock) }.from(%i[foo]).to(%i[foo foo]) + end + + it "stores arguments for a call" do + mock.bar(42) + args = Spectator::Arguments.capture(42) + call = mock._spectator_calls.first + expect(call.arguments).to eq(args) + end + end + end end end diff --git a/src/spectator/mocks/mock.cr b/src/spectator/mocks/mock.cr index 02160a1..25b44ca 100644 --- a/src/spectator/mocks/mock.cr +++ b/src/spectator/mocks/mock.cr @@ -3,6 +3,7 @@ require "./mocked" require "./reference_mock_registry" require "./stub" require "./stubbed_name" +require "./stubbed_type" require "./value_mock_registry" require "./value_stub" @@ -37,6 +38,7 @@ module Spectator {% if name %}@[::Spectator::StubbedName({{name}})]{% end %} {{base.id}} {{type_name.id}} < {{mocked_type.id}} include ::Spectator::Mocked + extend ::Spectator::StubbedType {% begin %} private getter(_spectator_stubs) do @@ -52,6 +54,10 @@ module Spectator @_spectator_stubs = nil end + private class_getter _spectator_stubs : Array(::Spectator::Stub) = [] of ::Spectator::Stub + + class_getter _spectator_calls : Array(::Spectator::MethodCall) = [] of ::Spectator::MethodCall + getter _spectator_calls = [] of ::Spectator::MethodCall # Returns the mock's name formatted for user output. @@ -111,6 +117,7 @@ module Spectator {% if name %}@[::Spectator::StubbedName({{name}})]{% end %} {{base.id}} {{type_name.id}} include ::Spectator::Mocked + extend ::Spectator::StubbedType {% if base == :class %} @@_spectator_mock_registry = ::Spectator::ReferenceMockRegistry.new @@ -120,6 +127,10 @@ module Spectator {% raise "Unsupported base type #{base} for injecting mock" %} {% end %} + private class_getter _spectator_stubs : Array(::Spectator::Stub) = [] of ::Spectator::Stub + + class_getter _spectator_calls : Array(::Spectator::MethodCall) = [] of ::Spectator::MethodCall + private def _spectator_stubs entry = @@_spectator_mock_registry.fetch(self) do _spectator_default_stubs