mirror of
https://gitea.invidious.io/iv-org/shard-spectator.git
synced 2024-08-15 00:53:35 +00:00
Support creating instances of mocked modules via class
This is a bit of a hack. The `.new` method is added to the module, which creates an instance that includes the mocked module. No changes to the def_mock and new_mock methods are necessary. For some reason, infinite recursion occurs when calling `.new` on the class. To get around the issue for now, the internal method of allocation is used. That is, allocate + initialize.
This commit is contained in:
parent
d378583054
commit
fa99987780
4 changed files with 388 additions and 0 deletions
|
@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
### Added
|
||||||
|
- Added support for mock modules and types that include mocked modules.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Fix macro logic to support free variables, 'self', and variants on stubbed methods. [#48](https://github.com/icy-arctic-fox/spectator/issues/48)
|
- Fix macro logic to support free variables, 'self', and variants on stubbed methods. [#48](https://github.com/icy-arctic-fox/spectator/issues/48)
|
||||||
- Fix method stubs used on methods that capture blocks.
|
- Fix method stubs used on methods that capture blocks.
|
||||||
|
|
|
@ -1027,4 +1027,262 @@ Spectator.describe "Mock DSL", :smoke do
|
||||||
expect(fake.reference).to eq("reference")
|
expect(fake.reference).to eq("reference")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "mock module" do
|
||||||
|
module Dummy
|
||||||
|
# `extend self` cannot be used.
|
||||||
|
# The Crystal compiler doesn't report the methods as class methods when doing so.
|
||||||
|
|
||||||
|
def self.abstract_method
|
||||||
|
:not_really_abstract
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.default_method
|
||||||
|
:original
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.args(arg)
|
||||||
|
arg
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.method1
|
||||||
|
:original
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.reference
|
||||||
|
method1.to_s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
mock(Dummy) do
|
||||||
|
abstract_stub def self.abstract_method
|
||||||
|
:abstract
|
||||||
|
end
|
||||||
|
|
||||||
|
stub def self.default_method
|
||||||
|
:default
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
let(fake) { class_mock(Dummy) }
|
||||||
|
|
||||||
|
it "raises on abstract stubs" do
|
||||||
|
expect { fake.abstract_method }.to raise_error(Spectator::UnexpectedMessage, /abstract_method/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can define default stubs" do
|
||||||
|
expect(fake.default_method).to eq(:default)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can define new stubs" do
|
||||||
|
expect { allow(fake).to receive(:args).and_return(42) }.to change { fake.args(5) }.from(5).to(42)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can override class method stubs" do
|
||||||
|
allow(fake).to receive(:method1).and_return(:override)
|
||||||
|
expect(fake.method1).to eq(:override)
|
||||||
|
end
|
||||||
|
|
||||||
|
xit "can reference stubs", pending: "Default stub of module class methods always refer to original" do
|
||||||
|
allow(fake).to receive(:method1).and_return(:reference)
|
||||||
|
expect(fake.reference).to eq("reference")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with a class including a mocked module" do
|
||||||
|
module Dummy
|
||||||
|
getter _spectator_invocations = [] of Symbol
|
||||||
|
|
||||||
|
def method1
|
||||||
|
@_spectator_invocations << :method1
|
||||||
|
"original"
|
||||||
|
end
|
||||||
|
|
||||||
|
def method2 : Symbol
|
||||||
|
@_spectator_invocations << :method2
|
||||||
|
:original
|
||||||
|
end
|
||||||
|
|
||||||
|
def method3(arg)
|
||||||
|
@_spectator_invocations << :method3
|
||||||
|
arg
|
||||||
|
end
|
||||||
|
|
||||||
|
def method4 : Symbol
|
||||||
|
@_spectator_invocations << :method4
|
||||||
|
yield
|
||||||
|
end
|
||||||
|
|
||||||
|
def method5
|
||||||
|
@_spectator_invocations << :method5
|
||||||
|
yield.to_i
|
||||||
|
end
|
||||||
|
|
||||||
|
def method6
|
||||||
|
@_spectator_invocations << :method6
|
||||||
|
yield
|
||||||
|
end
|
||||||
|
|
||||||
|
def method7(arg, *args, kwarg, **kwargs)
|
||||||
|
@_spectator_invocations << :method7
|
||||||
|
{arg, args, kwarg, kwargs}
|
||||||
|
end
|
||||||
|
|
||||||
|
def method8(arg, *args, kwarg, **kwargs)
|
||||||
|
@_spectator_invocations << :method8
|
||||||
|
yield
|
||||||
|
{arg, args, kwarg, kwargs}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# method1 stubbed via mock block
|
||||||
|
# method2 stubbed via keyword args
|
||||||
|
# method3 not stubbed (calls original)
|
||||||
|
# method4 stubbed via mock block (yields)
|
||||||
|
# method5 stubbed via keyword args (yields)
|
||||||
|
# method6 not stubbed (calls original and yields)
|
||||||
|
# method7 not stubbed (calls original) testing args
|
||||||
|
# method8 not stubbed (calls original and yields) testing args
|
||||||
|
mock(Dummy, method2: :stubbed, method5: 42) do
|
||||||
|
stub def method1
|
||||||
|
"stubbed"
|
||||||
|
end
|
||||||
|
|
||||||
|
stub def method4 : Symbol
|
||||||
|
yield
|
||||||
|
:block
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
subject(fake) { mock(Dummy) }
|
||||||
|
|
||||||
|
it "defines a subclass" do
|
||||||
|
expect(fake).to be_a(Dummy)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "defines stubs in the block" do
|
||||||
|
expect(fake.method1).to eq("stubbed")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can stub methods defined in the block" do
|
||||||
|
stub = Spectator::ValueStub.new(:method1, "override")
|
||||||
|
expect { fake._spectator_define_stub(stub) }.to change { fake.method1 }.from("stubbed").to("override")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "defines stubs from keyword arguments" do
|
||||||
|
expect(fake.method2).to eq(:stubbed)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can stub methods from keyword arguments" do
|
||||||
|
stub = Spectator::ValueStub.new(:method2, :override)
|
||||||
|
expect { fake._spectator_define_stub(stub) }.to change { fake.method2 }.from(:stubbed).to(:override)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "calls the original implementation for methods not provided a stub" do
|
||||||
|
expect(fake.method3(:xyz)).to eq(:xyz)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can stub methods after declaration" do
|
||||||
|
stub = Spectator::ValueStub.new(:method3, :abc)
|
||||||
|
expect { fake._spectator_define_stub(stub) }.to change { fake.method3(:xyz) }.from(:xyz).to(:abc)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "defines stubs with yield in the block" do
|
||||||
|
expect(fake.method4 { :wrong }).to eq(:block)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can stub methods with yield in the block" do
|
||||||
|
stub = Spectator::ValueStub.new(:method4, :override)
|
||||||
|
expect { fake._spectator_define_stub(stub) }.to change { fake.method4 { :wrong } }.from(:block).to(:override)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "defines stubs with yield from keyword arguments" do
|
||||||
|
expect(fake.method5 { :wrong }).to eq(42)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can stub methods with yield from keyword arguments" do
|
||||||
|
stub = Spectator::ValueStub.new(:method5, 123)
|
||||||
|
expect { fake._spectator_define_stub(stub) }.to change { fake.method5 { "0" } }.from(42).to(123)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can stub yielding methods after declaration" do
|
||||||
|
stub = Spectator::ValueStub.new(:method6, :abc)
|
||||||
|
expect { fake._spectator_define_stub(stub) }.to change { fake.method6 { :xyz } }.from(:xyz).to(:abc)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "handles arguments correctly" do
|
||||||
|
args1 = fake.method7(1, 2, 3, kwarg: 4, x: 5, y: 6, z: 7)
|
||||||
|
args2 = fake.method8(1, 2, 3, kwarg: 4, x: 5, y: 6, z: 7) { :block }
|
||||||
|
aggregate_failures do
|
||||||
|
expect(args1).to eq({1, {2, 3}, 4, {x: 5, y: 6, z: 7}})
|
||||||
|
expect(args2).to eq({1, {2, 3}, 4, {x: 5, y: 6, z: 7}})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "handles arguments correctly with stubs" do
|
||||||
|
stub1 = Spectator::ProcStub.new(:method7, args_proc)
|
||||||
|
stub2 = Spectator::ProcStub.new(:method8, args_proc)
|
||||||
|
fake._spectator_define_stub(stub1)
|
||||||
|
fake._spectator_define_stub(stub2)
|
||||||
|
args1 = fake.method7(1, 2, 3, kwarg: 4, x: 5, y: 6, z: 7)
|
||||||
|
args2 = fake.method8(1, 2, 3, kwarg: 4, x: 5, y: 6, z: 7) { :block }
|
||||||
|
aggregate_failures do
|
||||||
|
expect(args1).to eq({1, {2, 3}, 4, {x: 5, y: 6, z: 7}})
|
||||||
|
expect(args2).to eq({1, {2, 3}, 4, {x: 5, y: 6, z: 7}})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "compiles types without unions" do
|
||||||
|
aggregate_failures do
|
||||||
|
expect(fake.method1).to compile_as(String)
|
||||||
|
expect(fake.method2).to compile_as(Symbol)
|
||||||
|
expect(fake.method3(42)).to compile_as(Int32)
|
||||||
|
expect(fake.method4 { :foo }).to compile_as(Symbol)
|
||||||
|
expect(fake.method5 { "123" }).to compile_as(Int32)
|
||||||
|
expect(fake.method6 { "123" }).to compile_as(String)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def restricted(thing : Dummy)
|
||||||
|
thing.method1
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can be used in type restricted methods" do
|
||||||
|
expect(restricted(fake)).to eq("stubbed")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "does not call the original method when stubbed" do
|
||||||
|
fake.method1
|
||||||
|
fake.method2
|
||||||
|
fake.method3("foo")
|
||||||
|
fake.method4 { :foo }
|
||||||
|
fake.method5 { "42" }
|
||||||
|
fake.method6 { 42 }
|
||||||
|
fake.method7(1, 2, 3, kwarg: 4, x: 5, y: 6, z: 7)
|
||||||
|
fake.method8(1, 2, 3, kwarg: 4, x: 5, y: 6, z: 7) { :block }
|
||||||
|
expect(fake._spectator_invocations).to contain_exactly(:method3, :method6, :method7, :method8)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Cannot test unexpected messages - will not compile due to missing methods.
|
||||||
|
|
||||||
|
describe "deferred default stubs" do
|
||||||
|
mock(Dummy)
|
||||||
|
|
||||||
|
let(fake2) do
|
||||||
|
mock(Dummy,
|
||||||
|
method1: "stubbed",
|
||||||
|
method3: 123,
|
||||||
|
method4: :xyz)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "uses the keyword arguments as stubs" do
|
||||||
|
aggregate_failures do
|
||||||
|
expect(fake2.method1).to eq("stubbed")
|
||||||
|
expect(fake2.method2).to eq(:original)
|
||||||
|
expect(fake2.method3(42)).to eq(123)
|
||||||
|
expect(fake2.method4 { :foo }).to eq(:xyz)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -39,6 +39,29 @@ module Spectator
|
||||||
{% if base.id == :module.id %}
|
{% if base.id == :module.id %}
|
||||||
{{base.id}} {{type_name.id}}
|
{{base.id}} {{type_name.id}}
|
||||||
include {{mocked_type.id}}
|
include {{mocked_type.id}}
|
||||||
|
|
||||||
|
# Mock class that includes the mocked module {{mocked_type.id}}
|
||||||
|
{% if name %}@[::Spectator::StubbedName({{name}})]{% end %}
|
||||||
|
private class ClassIncludingMock{{type_name.id}}
|
||||||
|
include {{type_name.id}}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns a mock class that includes the mocked module {{mocked_type.id}}.
|
||||||
|
def self.new(*args, **kwargs) : ClassIncludingMock{{type_name.id}}
|
||||||
|
# FIXME: Creating the instance normally with `.new` causing infinite recursion.
|
||||||
|
inst = ClassIncludingMock{{type_name.id}}.allocate
|
||||||
|
inst.initialize(*args, **kwargs)
|
||||||
|
inst
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns a mock class that includes the mocked module {{mocked_type.id}}.
|
||||||
|
def self.new(*args, **kwargs) : ClassIncludingMock{{type_name.id}}
|
||||||
|
# FIXME: Creating the instance normally with `.new` causing infinite recursion.
|
||||||
|
inst = ClassIncludingMock{{type_name.id}}.allocate
|
||||||
|
inst.initialize(*args, **kwargs) { |*yargs| yield *yargs }
|
||||||
|
inst
|
||||||
|
end
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
{{base.id}} {{type_name.id}} < {{mocked_type.id}}
|
{{base.id}} {{type_name.id}} < {{mocked_type.id}}
|
||||||
{% end %}
|
{% end %}
|
||||||
|
|
104
test.cr
Normal file
104
test.cr
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
require "./src/spectator"
|
||||||
|
|
||||||
|
module Thing
|
||||||
|
def self.original_method
|
||||||
|
:original
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.default_method
|
||||||
|
:original
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.stubbed_method(_value = 42)
|
||||||
|
:original
|
||||||
|
end
|
||||||
|
|
||||||
|
macro finished
|
||||||
|
def self.debug
|
||||||
|
{% begin %}puts "Methods: ", {{@type.methods.map &.name.stringify}} of String{% end %}
|
||||||
|
{% begin %}puts "Class Methods: ", {{@type.class.methods.map &.name.stringify}} of String{% end %}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Thing.debug
|
||||||
|
|
||||||
|
# Spectator::Mock.define_subtype(:module, Thing, MockThing, default_method: :default) do
|
||||||
|
# stub def stubbed_method(_value = 42)
|
||||||
|
# :stubbed
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
|
||||||
|
# Spectator.describe "Mock modules" do
|
||||||
|
# let(mock) { MockThing }
|
||||||
|
|
||||||
|
# after { mock._spectator_clear_stubs }
|
||||||
|
|
||||||
|
# it "overrides an existing method" do
|
||||||
|
# stub = Spectator::ValueStub.new(:original_method, :override)
|
||||||
|
# expect { mock._spectator_define_stub(stub) }.to change { mock.original_method }.from(:original).to(:override)
|
||||||
|
# end
|
||||||
|
|
||||||
|
# it "doesn't affect other methods" do
|
||||||
|
# stub = Spectator::ValueStub.new(:stubbed_method, :override)
|
||||||
|
# expect { mock._spectator_define_stub(stub) }.to_not change { mock.original_method }
|
||||||
|
# end
|
||||||
|
|
||||||
|
# it "replaces an existing default stub" do
|
||||||
|
# stub = Spectator::ValueStub.new(:default_method, :override)
|
||||||
|
# expect { mock._spectator_define_stub(stub) }.to change { mock.default_method }.to(:override)
|
||||||
|
# end
|
||||||
|
|
||||||
|
# it "replaces an existing stubbed method" do
|
||||||
|
# stub = Spectator::ValueStub.new(:stubbed_method, :override)
|
||||||
|
# expect { mock._spectator_define_stub(stub) }.to change { mock.stubbed_method }.to(:override)
|
||||||
|
# end
|
||||||
|
|
||||||
|
# def restricted(thing : Thing.class)
|
||||||
|
# thing.default_method
|
||||||
|
# end
|
||||||
|
|
||||||
|
# describe "._spectator_clear_stubs" do
|
||||||
|
# before do
|
||||||
|
# stub = Spectator::ValueStub.new(:original_method, :override)
|
||||||
|
# mock._spectator_define_stub(stub)
|
||||||
|
# end
|
||||||
|
|
||||||
|
# it "removes previously defined stubs" do
|
||||||
|
# expect { mock._spectator_clear_stubs }.to change { mock.original_method }.from(:override).to(:original)
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
|
||||||
|
# describe "._spectator_calls" do
|
||||||
|
# before { 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 original methods" do
|
||||||
|
# expect { mock.original_method }.to change { called_method_names(mock) }.from(%i[]).to(%i[original_method])
|
||||||
|
# end
|
||||||
|
|
||||||
|
# it "stores calls to default methods" do
|
||||||
|
# expect { mock.default_method }.to change { called_method_names(mock) }.from(%i[]).to(%i[default_method])
|
||||||
|
# end
|
||||||
|
|
||||||
|
# it "stores calls to stubbed methods" do
|
||||||
|
# expect { mock.stubbed_method }.to change { called_method_names(mock) }.from(%i[]).to(%i[stubbed_method])
|
||||||
|
# end
|
||||||
|
|
||||||
|
# it "stores multiple calls to the same stub" do
|
||||||
|
# mock.stubbed_method
|
||||||
|
# expect { mock.stubbed_method }.to change { called_method_names(mock) }.from(%i[stubbed_method]).to(%i[stubbed_method stubbed_method])
|
||||||
|
# end
|
||||||
|
|
||||||
|
# it "stores arguments for a call" do
|
||||||
|
# mock.stubbed_method(5)
|
||||||
|
# args = Spectator::Arguments.capture(5)
|
||||||
|
# call = mock._spectator_calls.first
|
||||||
|
# expect(call.arguments).to eq(args)
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
# end
|
Loading…
Reference in a new issue