Skip to content

Commit

Permalink
Merge pull request #27 from Quick/property_spy
Browse files Browse the repository at this point in the history
PropertySpy property wrapper
  • Loading branch information
younata authored Dec 23, 2024
2 parents bddefe5 + 867ded7 commit 9eb0edc
Show file tree
Hide file tree
Showing 3 changed files with 218 additions and 6 deletions.
12 changes: 6 additions & 6 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,26 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/mattgallagher/CwlCatchException.git",
"state" : {
"revision" : "3ef6999c73b6938cc0da422f2c912d0158abb0a0",
"version" : "2.2.0"
"revision" : "07b2ba21d361c223e25e3c1e924288742923f08c",
"version" : "2.2.1"
}
},
{
"identity" : "cwlpreconditiontesting",
"kind" : "remoteSourceControl",
"location" : "https://github.com/mattgallagher/CwlPreconditionTesting.git",
"state" : {
"revision" : "2ef56b2caf25f55fa7eef8784c30d5a767550f54",
"version" : "2.2.1"
"revision" : "0139c665ebb45e6a9fbdb68aabfd7c39f3fe0071",
"version" : "2.2.2"
}
},
{
"identity" : "nimble",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Quick/Nimble.git",
"state" : {
"revision" : "1c49fc1243018f81a7ea99cb5e0985b00096e9f4",
"version" : "13.3.0"
"revision" : "7795df4fff1a9cd231fe4867ae54f4dc5f5734f9",
"version" : "13.7.1"
}
},
{
Expand Down
78 changes: 78 additions & 0 deletions Sources/Fakes/PropertySpy/PropertySpy.swift
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 }
}
}
134 changes: 134 additions & 0 deletions Tests/FakesTests/PropertySpyTests.swift
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 }
}

0 comments on commit 9eb0edc

Please sign in to comment.