Skip to content

Commit 9eb0edc

Browse files
authored
Merge pull request #27 from Quick/property_spy
PropertySpy property wrapper
2 parents bddefe5 + 867ded7 commit 9eb0edc

File tree

3 files changed

+218
-6
lines changed

3 files changed

+218
-6
lines changed

Package.resolved

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/// An immutable property spy.
2+
@propertyWrapper public struct PropertySpy<T, U> {
3+
public var wrappedValue: U {
4+
mapping(projectedValue())
5+
}
6+
7+
/// the ``Spy`` recording the getter calls.
8+
public let projectedValue: Spy<Void, T>
9+
10+
public let mapping: @Sendable (T) -> U
11+
12+
/// Creates an immutable PropertySpy stubbed with the given value, which lets you map from one type to another
13+
///
14+
/// - parameter value: The initial value to be stubbed
15+
/// - parameter mapping: A closure to map from the initial value to the property's return type
16+
///
17+
/// - Note: This initializer is particularly useful when the property is returning a protocol of some value, but you want to stub it with a particular instance of the protocol.
18+
public init(_ value: T, as mapping: @escaping @Sendable (T) -> U) {
19+
projectedValue = Spy(value)
20+
self.mapping = mapping
21+
}
22+
23+
/// Creates an immutable PropertySpy stubbed with the given value
24+
///
25+
/// - parameter value: The initial value to be stubbed
26+
public init(_ value: T) where T == U {
27+
projectedValue = Spy(value)
28+
self.mapping = { $0 }
29+
}
30+
}
31+
32+
/// A mutable property spy.
33+
@propertyWrapper public struct SettablePropertySpy<T, U> {
34+
public var wrappedValue: U {
35+
get {
36+
getMapping(projectedValue.getter())
37+
}
38+
set {
39+
projectedValue.setter(newValue)
40+
projectedValue.getter.stub(setMapping(newValue))
41+
}
42+
}
43+
44+
public struct ProjectedValue {
45+
/// A ``Spy`` recording every time the property has been set, with whatever the new value is, prior to mapping
46+
public let setter: Spy<U, Void>
47+
/// A ``Spy`` recording every time the property has been called. It is re-stubbed whenever the property's setter is called.
48+
public let getter: Spy<Void, T>
49+
}
50+
51+
/// The spies recording the setter and getter calls.
52+
public let projectedValue: ProjectedValue
53+
54+
public let getMapping: @Sendable (T) -> U
55+
public let setMapping: @Sendable (U) -> T
56+
57+
/// Creates a mutable PropertySpy stubbed with the given value, which lets you map from one type to another and back again
58+
///
59+
/// - parameter value: The initial value to be stubbed
60+
/// - parameter getMapping: A closure to map from the initial value to the property's return type
61+
/// - parameter setMapping: A closure to map from the property's return type back to the initial value's type.
62+
///
63+
/// - Note: This initializer is particularly useful when the property is returning a protocol of some value, but you want to stub it with a particular instance of the protocol.
64+
public init(_ value: T, getMapping: @escaping @Sendable (T) -> U, setMapping: @escaping @Sendable (U) -> T) {
65+
projectedValue = ProjectedValue(setter: Spy(), getter: Spy(value))
66+
self.getMapping = getMapping
67+
self.setMapping = setMapping
68+
}
69+
70+
/// Creatse a mutable PropertySpy stubbed with the given value
71+
///
72+
/// - parameter value: The inital value to be stubbed
73+
public init(_ value: T) where T == U {
74+
projectedValue = ProjectedValue(setter: Spy(), getter: Spy(value))
75+
self.getMapping = { $0 }
76+
self.setMapping = { $0 }
77+
}
78+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import Fakes
2+
import Nimble
3+
import XCTest
4+
5+
final class SettablePropertySpyTests: XCTestCase {
6+
func testGettingPropertyWhenTypesMatch() {
7+
struct AnObject {
8+
@SettablePropertySpy(1)
9+
var value: Int
10+
}
11+
12+
let object = AnObject()
13+
14+
expect(object.value).to(equal(1))
15+
// because we called it, we should expect for the getter spy to be called
16+
expect(object.$value.getter).to(beCalled())
17+
18+
// We never interacted with the setter, so it shouldn't have been called.
19+
expect(object.$value.setter).toNot(beCalled())
20+
}
21+
22+
func testSettingPropertyWhenTypesMatch() {
23+
struct AnObject {
24+
@SettablePropertySpy(1)
25+
var value: Int
26+
}
27+
28+
var object = AnObject()
29+
object.value = 3
30+
31+
expect(object.$value.getter).toNot(beCalled())
32+
expect(object.$value.setter).to(beCalled(3))
33+
34+
// the returned value should now be updated with the new value
35+
expect(object.value).to(equal(3))
36+
37+
// and because we called the getter, the getter spy should be called.
38+
expect(object.$value.getter).to(beCalled())
39+
}
40+
41+
func testGettingPropertyProtocolInheritence() {
42+
struct ImplementedProtocol: SomeProtocol {
43+
var value: Int = 1
44+
}
45+
46+
struct AnObject {
47+
@SettablePropertySpy(ImplementedProtocol(value: 2))
48+
var value: SomeProtocol
49+
}
50+
51+
let object = AnObject()
52+
53+
expect(object.value).to(beAKindOf(ImplementedProtocol.self))
54+
// because we called it, we should expect for the getter spy to be called
55+
expect(object.$value.getter).to(beCalled())
56+
57+
// We never interacted with the setter, so it shouldn't have been called.
58+
expect(object.$value.setter).toNot(beCalled())
59+
}
60+
61+
func testSettingPropertyProtocolInheritence() {
62+
struct ImplementedProtocol: SomeProtocol, Equatable {
63+
var value: Int = 1
64+
}
65+
66+
struct AnObject {
67+
@SettablePropertySpy(ImplementedProtocol())
68+
var value: SomeProtocol
69+
}
70+
71+
var object = AnObject()
72+
object.value = ImplementedProtocol(value: 2)
73+
74+
expect(object.$value.getter).toNot(beCalled())
75+
expect(object.$value.setter).to(beCalled(satisfyAllOf(
76+
beAKindOf(ImplementedProtocol.self),
77+
map(\.value, equal(2))
78+
)))
79+
80+
// the returned value should now be updated with the new value
81+
expect(object.value).to(satisfyAllOf(
82+
beAKindOf(ImplementedProtocol.self),
83+
map(\.value, equal(2))
84+
))
85+
// and because we called the getter, the getter spy should be called.
86+
expect(object.$value.getter).to(beCalled(times: 1))
87+
}
88+
}
89+
90+
final class PropertySpyTests: XCTestCase {
91+
func testGettingPropertyWhenTypesMatch() {
92+
struct AnObject {
93+
@PropertySpy(1)
94+
var value: Int
95+
}
96+
97+
let object = AnObject()
98+
99+
expect(object.value).to(equal(1))
100+
// because we called it, we should expect for the getter spy to be called
101+
expect(object.$value).to(beCalled())
102+
}
103+
104+
func testGettingPropertyProtocolInheritence() {
105+
struct ImplementedProtocol: SomeProtocol {
106+
var value: Int = 1
107+
}
108+
109+
struct ObjectUsingProtocol {
110+
@PropertySpy(ImplementedProtocol(value: 2))
111+
var value: SomeProtocol
112+
}
113+
114+
struct ObjectUsingDirectInstance {
115+
@PropertySpy(ImplementedProtocol(value: 2), as: { $0 })
116+
var value: SomeProtocol
117+
}
118+
119+
let object = ObjectUsingProtocol()
120+
121+
expect(object.value).to(beAnInstanceOf(ImplementedProtocol.self))
122+
// because we called it, we should expect for the getter spy to be called
123+
expect(object.$value).to(beCalled())
124+
expect(object.$value).to(beAnInstanceOf(Spy<Void, SomeProtocol>.self))
125+
126+
let otherObject = ObjectUsingDirectInstance()
127+
128+
expect(otherObject.$value).to(beAnInstanceOf(Spy<Void, ImplementedProtocol>.self))
129+
}
130+
}
131+
132+
protocol SomeProtocol {
133+
var value: Int { get }
134+
}

0 commit comments

Comments
 (0)