-
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #27 from Quick/property_spy
PropertySpy property wrapper
- Loading branch information
Showing
3 changed files
with
218 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
/// An immutable property spy. | ||
@propertyWrapper public struct PropertySpy<T, U> { | ||
public var wrappedValue: U { | ||
mapping(projectedValue()) | ||
} | ||
|
||
/// the ``Spy`` recording the getter calls. | ||
public let projectedValue: Spy<Void, T> | ||
|
||
public let mapping: @Sendable (T) -> U | ||
|
||
/// Creates an immutable PropertySpy stubbed with the given value, which lets you map from one type to another | ||
/// | ||
/// - parameter value: The initial value to be stubbed | ||
/// - parameter mapping: A closure to map from the initial value to the property's return type | ||
/// | ||
/// - 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. | ||
public init(_ value: T, as mapping: @escaping @Sendable (T) -> U) { | ||
projectedValue = Spy(value) | ||
self.mapping = mapping | ||
} | ||
|
||
/// Creates an immutable PropertySpy stubbed with the given value | ||
/// | ||
/// - parameter value: The initial value to be stubbed | ||
public init(_ value: T) where T == U { | ||
projectedValue = Spy(value) | ||
self.mapping = { $0 } | ||
} | ||
} | ||
|
||
/// A mutable property spy. | ||
@propertyWrapper public struct SettablePropertySpy<T, U> { | ||
public var wrappedValue: U { | ||
get { | ||
getMapping(projectedValue.getter()) | ||
} | ||
set { | ||
projectedValue.setter(newValue) | ||
projectedValue.getter.stub(setMapping(newValue)) | ||
} | ||
} | ||
|
||
public struct ProjectedValue { | ||
/// A ``Spy`` recording every time the property has been set, with whatever the new value is, prior to mapping | ||
public let setter: Spy<U, Void> | ||
/// A ``Spy`` recording every time the property has been called. It is re-stubbed whenever the property's setter is called. | ||
public let getter: Spy<Void, T> | ||
} | ||
|
||
/// The spies recording the setter and getter calls. | ||
public let projectedValue: ProjectedValue | ||
|
||
public let getMapping: @Sendable (T) -> U | ||
public let setMapping: @Sendable (U) -> T | ||
|
||
/// Creates a mutable PropertySpy stubbed with the given value, which lets you map from one type to another and back again | ||
/// | ||
/// - parameter value: The initial value to be stubbed | ||
/// - parameter getMapping: A closure to map from the initial value to the property's return type | ||
/// - parameter setMapping: A closure to map from the property's return type back to the initial value's type. | ||
/// | ||
/// - 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. | ||
public init(_ value: T, getMapping: @escaping @Sendable (T) -> U, setMapping: @escaping @Sendable (U) -> T) { | ||
projectedValue = ProjectedValue(setter: Spy(), getter: Spy(value)) | ||
self.getMapping = getMapping | ||
self.setMapping = setMapping | ||
} | ||
|
||
/// Creatse a mutable PropertySpy stubbed with the given value | ||
/// | ||
/// - parameter value: The inital value to be stubbed | ||
public init(_ value: T) where T == U { | ||
projectedValue = ProjectedValue(setter: Spy(), getter: Spy(value)) | ||
self.getMapping = { $0 } | ||
self.setMapping = { $0 } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
import Fakes | ||
import Nimble | ||
import XCTest | ||
|
||
final class SettablePropertySpyTests: XCTestCase { | ||
func testGettingPropertyWhenTypesMatch() { | ||
struct AnObject { | ||
@SettablePropertySpy(1) | ||
var value: Int | ||
} | ||
|
||
let object = AnObject() | ||
|
||
expect(object.value).to(equal(1)) | ||
// because we called it, we should expect for the getter spy to be called | ||
expect(object.$value.getter).to(beCalled()) | ||
|
||
// We never interacted with the setter, so it shouldn't have been called. | ||
expect(object.$value.setter).toNot(beCalled()) | ||
} | ||
|
||
func testSettingPropertyWhenTypesMatch() { | ||
struct AnObject { | ||
@SettablePropertySpy(1) | ||
var value: Int | ||
} | ||
|
||
var object = AnObject() | ||
object.value = 3 | ||
|
||
expect(object.$value.getter).toNot(beCalled()) | ||
expect(object.$value.setter).to(beCalled(3)) | ||
|
||
// the returned value should now be updated with the new value | ||
expect(object.value).to(equal(3)) | ||
|
||
// and because we called the getter, the getter spy should be called. | ||
expect(object.$value.getter).to(beCalled()) | ||
} | ||
|
||
func testGettingPropertyProtocolInheritence() { | ||
struct ImplementedProtocol: SomeProtocol { | ||
var value: Int = 1 | ||
} | ||
|
||
struct AnObject { | ||
@SettablePropertySpy(ImplementedProtocol(value: 2)) | ||
var value: SomeProtocol | ||
} | ||
|
||
let object = AnObject() | ||
|
||
expect(object.value).to(beAKindOf(ImplementedProtocol.self)) | ||
// because we called it, we should expect for the getter spy to be called | ||
expect(object.$value.getter).to(beCalled()) | ||
|
||
// We never interacted with the setter, so it shouldn't have been called. | ||
expect(object.$value.setter).toNot(beCalled()) | ||
} | ||
|
||
func testSettingPropertyProtocolInheritence() { | ||
struct ImplementedProtocol: SomeProtocol, Equatable { | ||
var value: Int = 1 | ||
} | ||
|
||
struct AnObject { | ||
@SettablePropertySpy(ImplementedProtocol()) | ||
var value: SomeProtocol | ||
} | ||
|
||
var object = AnObject() | ||
object.value = ImplementedProtocol(value: 2) | ||
|
||
expect(object.$value.getter).toNot(beCalled()) | ||
expect(object.$value.setter).to(beCalled(satisfyAllOf( | ||
beAKindOf(ImplementedProtocol.self), | ||
map(\.value, equal(2)) | ||
))) | ||
|
||
// the returned value should now be updated with the new value | ||
expect(object.value).to(satisfyAllOf( | ||
beAKindOf(ImplementedProtocol.self), | ||
map(\.value, equal(2)) | ||
)) | ||
// and because we called the getter, the getter spy should be called. | ||
expect(object.$value.getter).to(beCalled(times: 1)) | ||
} | ||
} | ||
|
||
final class PropertySpyTests: XCTestCase { | ||
func testGettingPropertyWhenTypesMatch() { | ||
struct AnObject { | ||
@PropertySpy(1) | ||
var value: Int | ||
} | ||
|
||
let object = AnObject() | ||
|
||
expect(object.value).to(equal(1)) | ||
// because we called it, we should expect for the getter spy to be called | ||
expect(object.$value).to(beCalled()) | ||
} | ||
|
||
func testGettingPropertyProtocolInheritence() { | ||
struct ImplementedProtocol: SomeProtocol { | ||
var value: Int = 1 | ||
} | ||
|
||
struct ObjectUsingProtocol { | ||
@PropertySpy(ImplementedProtocol(value: 2)) | ||
var value: SomeProtocol | ||
} | ||
|
||
struct ObjectUsingDirectInstance { | ||
@PropertySpy(ImplementedProtocol(value: 2), as: { $0 }) | ||
var value: SomeProtocol | ||
} | ||
|
||
let object = ObjectUsingProtocol() | ||
|
||
expect(object.value).to(beAnInstanceOf(ImplementedProtocol.self)) | ||
// because we called it, we should expect for the getter spy to be called | ||
expect(object.$value).to(beCalled()) | ||
expect(object.$value).to(beAnInstanceOf(Spy<Void, SomeProtocol>.self)) | ||
|
||
let otherObject = ObjectUsingDirectInstance() | ||
|
||
expect(otherObject.$value).to(beAnInstanceOf(Spy<Void, ImplementedProtocol>.self)) | ||
} | ||
} | ||
|
||
protocol SomeProtocol { | ||
var value: Int { get } | ||
} |