Skip to content

Commit 610ae92

Browse files
committed
Add conveniences to aid those using Spy without Nimble
1 parent 28e5f84 commit 610ae92

File tree

6 files changed

+197
-64
lines changed

6 files changed

+197
-64
lines changed

.github/workflows/ci-swiftpm.yml

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,25 @@ jobs:
2626
runs-on: macos-14
2727
strategy:
2828
matrix:
29-
xcode: ["15.3", "16.1"]
29+
xcode: ["16.1"]
30+
fail-fast: false
31+
env:
32+
DEVELOPER_DIR: "/Applications/Xcode_${{ matrix.xcode }}.app"
33+
steps:
34+
- uses: actions/checkout@v4
35+
if: ${{ needs.filter.outputs.should_skip != 'true' }}
36+
- run: swift build
37+
if: ${{ needs.filter.outputs.should_skip != 'true' }}
38+
- run: swift test
39+
if: ${{ needs.filter.outputs.should_skip != 'true' }}
40+
41+
swiftpm_darwin_15:
42+
name: SwiftPM, Darwin 15, Xcode ${{ matrix.xcode }}
43+
needs: filter
44+
runs-on: macos-15
45+
strategy:
46+
matrix:
47+
xcode: ["16.4"]
3048
fail-fast: false
3149
env:
3250
DEVELOPER_DIR: "/Applications/Xcode_${{ matrix.xcode }}.app"
@@ -45,9 +63,9 @@ jobs:
4563
strategy:
4664
matrix:
4765
container:
48-
- swift:5.9
49-
- swift:5.10
5066
- swift:6.0
67+
- swift:6.1
68+
- swift:6.2
5169
fail-fast: false
5270
container: ${{ matrix.container }}
5371
steps:

Package.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
// swift-tools-version: 5.7
1+
// swift-tools-version: 6.0
22

33
import PackageDescription
44

55
let package = Package(
66
name: "swift-fakes",
77
platforms: [
8-
.macOS(.v10_15), .iOS(.v13), .tvOS(.v13)
8+
.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .visionOS(.v1)
99
],
1010
products: [
1111
.library(

[email protected]

Lines changed: 0 additions & 35 deletions
This file was deleted.

Sources/Fakes/Spy/Spy+Result.swift

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,27 +20,17 @@ extension Spy {
2020
self.init(.failure(EmptyError()))
2121
}
2222

23-
#if swift(>=6.0)
2423
public convenience init<Success, Failure: Error>(_ closure: @escaping @Sendable (Arguments) throws(Failure) -> Success) where Returning == Result<Success, Failure> {
2524
self.init { args in
2625
do {
2726
return .success(try closure(args))
28-
} catch let error {
27+
} catch let error as Failure {
2928
return .failure(error)
30-
}
31-
}
32-
}
33-
#else
34-
public convenience init<Success>(_ closure: @escaping @Sendable (Arguments) throws -> Success) where Returning == Result<Success, Swift.Error> {
35-
self.init { args in
36-
do {
37-
return .success(try closure(args))
3829
} catch let error {
39-
return .failure(error)
30+
fatalError("closure was typed to throw only errors of type \(Failure.self), got \(error). This shouldn't happen.")
4031
}
4132
}
4233
}
43-
#endif
4434
}
4535

4636
extension Spy {
@@ -65,27 +55,17 @@ extension Spy {
6555
self.stub(.failure(failure))
6656
}
6757

68-
#if swift(>=6.0)
6958
public func stub<Success, Failure: Error>(_ closure: @escaping @Sendable (Arguments) throws(Failure) -> Success) where Returning == Result<Success, Failure> {
7059
self.stub { args in
7160
do {
7261
return .success(try closure(args))
73-
} catch let error {
62+
} catch let error as Failure {
7463
return .failure(error)
75-
}
76-
}
77-
}
78-
#else
79-
public func stub<Success>(_ closure: @escaping @Sendable (Arguments) throws -> Success) where Returning == Result<Success, Swift.Error> {
80-
self.stub { args in
81-
do {
82-
return .success(try closure(args))
8364
} catch let error {
84-
return .failure(error)
65+
fatalError("closure was typed to throw only errors of type \(Failure.self), got \(error). This shouldn't happen.")
8566
}
8667
}
8768
}
88-
#endif
8969
}
9070

9171
extension Spy {
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
//
2+
// Spy+Testing.swift
3+
// swift-fakes
4+
//
5+
// Created by Rachel Brindle on 9/29/25.
6+
//
7+
8+
extension Spy {
9+
/// Returns true if this spy has been called at least once.
10+
public var wasCalled: Bool {
11+
calls.isEmpty == false
12+
}
13+
14+
/// Returns true if this spy has been called exactly as many times as specified.
15+
public func wasCalled(times: Int) -> Bool {
16+
calls.count == times
17+
}
18+
19+
/// Returns true if this spy has not been called.
20+
public var wasNotCalled: Bool {
21+
calls.isEmpty
22+
}
23+
24+
/// Returns whether this spy called at any time with the given value.
25+
public func wasCalled(with value: Arguments) -> Bool where Arguments: Equatable {
26+
calls.contains { call in
27+
call == value
28+
}
29+
}
30+
31+
/// Returns whether this spy called with precisely these values, in this order.
32+
public func wasCalled(with values: [Arguments]) -> Bool where Arguments: Equatable {
33+
let currentCalls = calls
34+
guard currentCalls.count == values.count else { return false }
35+
36+
for idx in 0..<currentCalls.count {
37+
if currentCalls[idx] != values[idx] { return false }
38+
}
39+
return true
40+
}
41+
42+
/// Returns whether this spy was called at any time with a value that matches the given closure.
43+
public func wasCalled(matching matcher: (Arguments) -> Bool) -> Bool {
44+
calls.contains { call in
45+
matcher(call)
46+
}
47+
}
48+
49+
/// Returns whether this spy was called with values that correspond to the order of closures given.
50+
///
51+
/// For example, if this spy was called with `[1, 2]`, then `wasCalled(matching: [{ $0 == 1 }, { $0 == 2}])` would return true.
52+
public func wasCalled(matching matchers: [(Arguments) -> Bool]) -> Bool {
53+
let currentCalls = calls
54+
guard currentCalls.count == matchers.count else { return false }
55+
56+
for idx in 0..<currentCalls.count {
57+
if matchers[idx](currentCalls[idx]) == false { return false }
58+
}
59+
return true
60+
}
61+
62+
/// Returns whether the most recent call to the spy equals the given value.
63+
public func wasMostRecentlyCalled(with value: Arguments) -> Bool where Arguments: Equatable {
64+
calls.last == value
65+
}
66+
67+
/// Returns whether the most recent call to the spy matches the given closure.
68+
public func wasMostRecentlyCalled(matching matcher: (Arguments) -> Bool) -> Bool where Arguments: Equatable {
69+
guard let lastCall = calls.last else { return false }
70+
return matcher(lastCall)
71+
}
72+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import Fakes
2+
import Testing
3+
4+
struct SpyTestHelpersTest {
5+
@Test func wasCalledWithoutArguments() {
6+
let spy = Spy<Int, Void>()
7+
8+
// beCalled should match if spy has been called any number of times.
9+
#expect(spy.wasCalled == false)
10+
#expect(spy.wasNotCalled)
11+
12+
spy(1)
13+
#expect(spy.wasCalled)
14+
#expect(spy.wasNotCalled == false)
15+
16+
spy(2)
17+
#expect(spy.wasCalled)
18+
#expect(spy.wasNotCalled == false)
19+
}
20+
21+
@Test func wasCalledWithArguments() {
22+
let spy = Spy<Int, Void>()
23+
24+
spy(3)
25+
#expect(spy.wasCalled(with: 3))
26+
#expect(spy.wasCalled(with: 2) == false)
27+
28+
#expect(spy.wasCalled(matching: { $0 == 3 }))
29+
#expect(spy.wasCalled(matching: { $0 == 2 }) == false)
30+
31+
spy(4)
32+
#expect(spy.wasCalled(with: 3))
33+
#expect(spy.wasCalled(matching: { $0 == 3 }))
34+
35+
#expect(spy.wasCalled(with: 4))
36+
#expect(spy.wasCalled(matching: { $0 == 4 }))
37+
}
38+
39+
@Test func wasCalledWithTimes() {
40+
let spy = Spy<Int, Void>()
41+
42+
#expect(spy.wasCalled(times: 0))
43+
#expect(spy.wasCalled(times: 1) == false)
44+
#expect(spy.wasCalled(times: 2) == false)
45+
46+
spy(1)
47+
#expect(spy.wasCalled(times: 0) == false)
48+
#expect(spy.wasCalled(times: 1))
49+
#expect(spy.wasCalled(times: 2) == false)
50+
51+
spy(2)
52+
#expect(spy.wasCalled(times: 0) == false)
53+
#expect(spy.wasCalled(times: 1) == false)
54+
#expect(spy.wasCalled(times: 2))
55+
}
56+
57+
@Test func wasCalledWithMultipleArgumentsAndTimes() {
58+
let spy = Spy<Int, Void>()
59+
60+
spy(1)
61+
spy(3)
62+
63+
#expect(spy.wasCalled(with: [1, 3]))
64+
#expect(spy.wasCalled(with: [3, 1]) == false) // order matters
65+
66+
#expect(spy.wasCalled(
67+
matching: [
68+
{ $0 == 1},
69+
{ $0 == 3 }
70+
]
71+
))
72+
#expect(spy.wasCalled(
73+
matching: [
74+
{ $0 == 3 },
75+
{ $0 == 1 }
76+
]
77+
) == false)
78+
}
79+
80+
@Test func testMostRecentlyBeCalled() {
81+
let spy = Spy<Int, Void>()
82+
83+
spy(1)
84+
#expect(spy.wasMostRecentlyCalled(with: 1))
85+
#expect(spy.wasMostRecentlyCalled(with: 2) == false)
86+
87+
#expect(spy.wasMostRecentlyCalled(matching: { $0 == 1}))
88+
#expect(spy.wasMostRecentlyCalled(matching: { $0 == 2}) == false)
89+
90+
spy(2)
91+
#expect(spy.wasMostRecentlyCalled(with: 1) == false)
92+
#expect(spy.wasMostRecentlyCalled(with: 2))
93+
94+
#expect(spy.wasMostRecentlyCalled(matching: { $0 == 1}) == false)
95+
#expect(spy.wasMostRecentlyCalled(matching: { $0 == 2}))
96+
}
97+
}
98+

0 commit comments

Comments
 (0)