mirror of
https://gitea.invidious.io/iv-org/shard-spectator.git
synced 2024-08-15 00:53:35 +00:00
Add ValueMockRegistry
Support injecting mock functionality into concrete structs (value types).
This commit is contained in:
parent
51f133eb61
commit
37c6db250d
4 changed files with 153 additions and 0 deletions
|
@ -16,6 +16,22 @@ class MockedClass
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
struct MockedStruct
|
||||||
|
getter method1 = 42
|
||||||
|
|
||||||
|
def method2
|
||||||
|
:original
|
||||||
|
end
|
||||||
|
|
||||||
|
def method3
|
||||||
|
"original"
|
||||||
|
end
|
||||||
|
|
||||||
|
def instance_variables
|
||||||
|
[{{@type.instance_vars.map(&.name.symbolize).splat}}]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
Spectator.describe Spectator::Mock do
|
Spectator.describe Spectator::Mock do
|
||||||
describe "#define_subclass" do
|
describe "#define_subclass" do
|
||||||
class Thing
|
class Thing
|
||||||
|
@ -106,5 +122,43 @@ Spectator.describe Spectator::Mock do
|
||||||
expect(mock.instance_variables).to eq([:method1])
|
expect(mock.instance_variables).to eq([:method1])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "with a struct" do
|
||||||
|
Spectator::Mock.inject(MockedStruct, :mock_name, method1: 123) do
|
||||||
|
stub def method2
|
||||||
|
:stubbed
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
let(mock) { MockedStruct.new }
|
||||||
|
|
||||||
|
it "overrides responses from methods with keyword arguments" do
|
||||||
|
expect(mock.method1).to eq(123)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "overrides responses from methods defined in the block" do
|
||||||
|
expect(mock.method2).to eq(:stubbed)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "allows methods to be stubbed" do
|
||||||
|
stub1 = Spectator::ValueStub.new(:method1, 777)
|
||||||
|
stub2 = Spectator::ValueStub.new(:method2, :override)
|
||||||
|
stub3 = Spectator::ValueStub.new(:method3, "stubbed")
|
||||||
|
|
||||||
|
aggregate_failures do
|
||||||
|
expect { mock._spectator_define_stub(stub1) }.to change { mock.method1 }.to(777)
|
||||||
|
expect { mock._spectator_define_stub(stub2) }.to change { mock.method2 }.to(:override)
|
||||||
|
expect { mock._spectator_define_stub(stub3) }.to change { mock.method3 }.from("original").to("stubbed")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't change the size of an instance" do
|
||||||
|
expect(sizeof(MockedStruct)).to eq(4) # sizeof(Int32)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't affect instance variables" do
|
||||||
|
expect(mock.instance_variables).to eq([:method1])
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
41
spec/spectator/mocks/value_mock_registry_spec.cr
Normal file
41
spec/spectator/mocks/value_mock_registry_spec.cr
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
require "../../spec_helper"
|
||||||
|
|
||||||
|
Spectator.describe Spectator::ValueMockRegistry do
|
||||||
|
subject(registry) { Spectator::ValueMockRegistry(Int32).new }
|
||||||
|
let(obj) { 42 }
|
||||||
|
let(stub) { Spectator::ValueStub.new(:test, 5) }
|
||||||
|
let(no_stubs) { [] of Spectator::Stub }
|
||||||
|
|
||||||
|
it "initially has no stubs" do
|
||||||
|
expect(registry[obj]).to be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
it "stores stubs for an object" do
|
||||||
|
expect { registry[obj] << stub }.to change { registry[obj] }.from(no_stubs).to([stub])
|
||||||
|
end
|
||||||
|
|
||||||
|
it "isolates stubs between different objects" do
|
||||||
|
obj1 = 1
|
||||||
|
obj2 = 2
|
||||||
|
registry[obj2] << Spectator::ValueStub.new(:obj2, 42)
|
||||||
|
expect { registry[obj1] << stub }.to_not change { registry[obj2] }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#fetch" do
|
||||||
|
it "retrieves existing stubs" do
|
||||||
|
registry[obj] << stub
|
||||||
|
expect(registry.fetch(obj) { no_stubs }).to eq([stub])
|
||||||
|
end
|
||||||
|
|
||||||
|
it "stores stubs on the first retrieval" do
|
||||||
|
expect(registry.fetch(obj) { [stub] of Spectator::Stub }).to eq([stub])
|
||||||
|
end
|
||||||
|
|
||||||
|
it "isolates stubs between different objects" do
|
||||||
|
obj1 = 1
|
||||||
|
obj2 = 2
|
||||||
|
registry[obj2] << Spectator::ValueStub.new(:obj2, 42)
|
||||||
|
expect { registry.fetch(obj1) { no_stubs } }.to_not change { registry[obj2] }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -3,6 +3,7 @@ require "./mocked"
|
||||||
require "./reference_mock_registry"
|
require "./reference_mock_registry"
|
||||||
require "./stub"
|
require "./stub"
|
||||||
require "./stubbed_name"
|
require "./stubbed_name"
|
||||||
|
require "./value_mock_registry"
|
||||||
require "./value_stub"
|
require "./value_stub"
|
||||||
|
|
||||||
module Spectator
|
module Spectator
|
||||||
|
@ -54,6 +55,8 @@ module Spectator
|
||||||
|
|
||||||
{% if type.class? %}
|
{% if type.class? %}
|
||||||
@@_spectator_mock_registry = ::Spectator::ReferenceMockRegistry.new
|
@@_spectator_mock_registry = ::Spectator::ReferenceMockRegistry.new
|
||||||
|
{% elsif type.struct? %}
|
||||||
|
@@_spectator_mock_registry = ::Spectator::ValueMockRegistry(self).new
|
||||||
{% else %}
|
{% else %}
|
||||||
{% raise "Unsupported type for injecting mock" %}
|
{% raise "Unsupported type for injecting mock" %}
|
||||||
{% end %}
|
{% end %}
|
||||||
|
|
55
src/spectator/mocks/value_mock_registry.cr
Normal file
55
src/spectator/mocks/value_mock_registry.cr
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
require "string_pool"
|
||||||
|
require "./stub"
|
||||||
|
|
||||||
|
module Spectator
|
||||||
|
# Stores collections of stubs for mocked value (struct) types.
|
||||||
|
#
|
||||||
|
# *T* is the type of value to track.
|
||||||
|
#
|
||||||
|
# This type is intended for all mocked struct types that have functionality "injected."
|
||||||
|
# That is, the type itself has mock functionality bolted on.
|
||||||
|
# Adding instance members should be avoided, for instance, it could mess up serialization.
|
||||||
|
# This registry works around that by mapping mocks (via their raw memory content) to a collection of stubs.
|
||||||
|
# Doing so prevents adding data to the mocked type.
|
||||||
|
class ValueMockRegistry(T)
|
||||||
|
@pool = StringPool.new # Used to de-dup values.
|
||||||
|
@object_stubs : Hash(String, Array(Stub))
|
||||||
|
|
||||||
|
# Creates an empty registry.
|
||||||
|
def initialize
|
||||||
|
@object_stubs = Hash(String, Array(Stub)).new do |hash, key|
|
||||||
|
hash[key] = [] of Stub
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Retrieves all stubs defined for a mocked object.
|
||||||
|
def [](object : T) : Array(Stub)
|
||||||
|
key = value_bytes(object)
|
||||||
|
@object_stubs[key]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Retrieves all stubs defined for a mocked object.
|
||||||
|
#
|
||||||
|
# Yields to the block on the first retrieval.
|
||||||
|
# This allows a mock to populate the registry with initial stubs.
|
||||||
|
def fetch(object : T, & : -> Array(Stub))
|
||||||
|
key = value_bytes(object)
|
||||||
|
@object_stubs.fetch(key) do
|
||||||
|
@object_stubs[key] = yield
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Extracts heap-managed bytes for a value.
|
||||||
|
#
|
||||||
|
# Strings are used because a string pool is used.
|
||||||
|
# However, the strings are treated as an array of bytes.
|
||||||
|
@[AlwaysInline]
|
||||||
|
private def value_bytes(value : T) : String
|
||||||
|
# Get slice pointing to the memory used by the value (does not allocate).
|
||||||
|
bytes = Bytes.new(pointerof(value).as(UInt8*), sizeof(T), read_only: true)
|
||||||
|
|
||||||
|
# De-dup the value (may allocate).
|
||||||
|
@pool.get(bytes)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue