Skip to content

Commit 174f882

Browse files
committed
DSL for SwiftRex functional tests
1 parent c162589 commit 174f882

File tree

2 files changed

+209
-48
lines changed

2 files changed

+209
-48
lines changed

README.md

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,126 @@
11
# TestingExtensions
2-
Testing helpers and extensions for SwiftRex
2+
Testing helpers and extensions for SwiftRex and Combine
3+
4+
## SwiftRex Functional Tests (Use Case)
5+
```swift
6+
let (dependencies, scheduler) = self.setup
7+
8+
assert(
9+
initialValue: AppState.initial,
10+
reducer: Reducer.myReducer.lift(action: \.actionReducerScope, state: \.stateReducerScope),
11+
middleware: MyHandler.middleware.inject(dependencies),
12+
steps: {
13+
Send(action: .certainAction(.verySpecific))
14+
15+
SideEffectResult {
16+
scheduler.advance(by: .seconds(5))
17+
}
18+
19+
Send(action: .anotherAction(.withSomeValues(list: [], date: dependencies.now())))
20+
21+
// if you receive an action and don't add this block, the test will fail to remind you
22+
Receive { action -> Bool in
23+
// validates that the received action is what you would expect
24+
// if this function returns false, the test will fail to show you that you've got an unexpected action
25+
if case let .my(.expectedAction(list)) = action {
26+
return list.isEmpty
27+
} else {
28+
return false
29+
}
30+
}
31+
32+
SideEffectResult {
33+
scheduler.advance(by: .seconds(5))
34+
}
35+
36+
Send(action: .anotherAction(.stopSomething))
37+
38+
SideEffectResult {
39+
scheduler.advance(by: .seconds(5))
40+
}
41+
42+
// if you receive an action and don't add this block, the test will fail to remind you
43+
Receive { action -> Bool in
44+
// validates that the received action is what you would expect
45+
// if this function returns false, the test will fail to show you that you've got an unexpected action
46+
if case let .my(.expectedAction(list)) = action {
47+
return list.isEmpty
48+
} else {
49+
return false
50+
}
51+
}
52+
}
53+
)
54+
```
55+
56+
## Combine
57+
58+
Validate Output of Publishers
59+
```swift
60+
let operation = assert(
61+
publisher: myPublisher,
62+
eventuallyReceives: "🙉", "🙊", "🙈",
63+
andCompletes: false
64+
)
65+
somethingHappensAndPublisherReceives("🙉")
66+
somethingHappensAndPublisherReceives("🙊")
67+
somethingHappensAndPublisherReceives("🙈")
68+
69+
operation.wait(0.0001)
70+
```
71+
72+
Validate Output and Successful Completion of Publishers
73+
```swift
74+
let operation = assert(
75+
publisher: myPublisher,
76+
eventuallyReceives: "🙉", "🙊", "🙈",
77+
andCompletesWith: .isSuccess
78+
)
79+
somethingHappensAndPublisherReceives("🙉")
80+
somethingHappensAndPublisherReceives("🙊")
81+
somethingHappensAndPublisherReceives("🙈")
82+
somethingHappensAndPublisherCompletes()
83+
84+
operation.wait(0.0001)
85+
```
86+
87+
Validate Output and some Failure of Publishers
88+
```swift
89+
let operation = assert(
90+
publisher: myPublisher,
91+
eventuallyReceives: "🙉", "🙊", "🙈",
92+
andCompletesWith: .isFailure
93+
)
94+
somethingHappensAndPublisherReceives("🙉")
95+
somethingHappensAndPublisherReceives("🙊")
96+
somethingHappensAndPublisherReceives("🙈")
97+
somethingHappensAndPublisherFails(SomeError())
98+
99+
operation.wait(0.0001)
100+
```
101+
102+
Validate Output and specific Failure of Publishers
103+
```swift
104+
let operation = assert(
105+
publisher: myPublisher,
106+
eventuallyReceives: "🙉", "🙊", "🙈",
107+
andCompletesWith: .failedWithError { error in error == SomeError("123") }
108+
)
109+
somethingHappensAndPublisherReceives("🙉")
110+
somethingHappensAndPublisherReceives("🙊")
111+
somethingHappensAndPublisherReceives("🙈")
112+
somethingHappensAndPublisherFails(SomeError("123"))
113+
114+
operation.wait(0.0001)
115+
```
116+
117+
Validate No Output but completion of Publishers
118+
```swift
119+
let operation = assert(
120+
publisher: myPublisher,
121+
completesWithoutValues: .isSuccess
122+
)
123+
somethingHappensAndPublisherCompletes()
124+
125+
operation.wait(0.0001)
126+
```

Sources/TestingExtensions/UseCaseTests.swift

Lines changed: 84 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,44 @@ import Foundation
1313
@testable import SwiftRex
1414
import XCTest
1515

16-
public struct SendStep<ActionType, StateType> {
16+
@resultBuilder public struct StepBuilder<Action, State> {
17+
public static func buildBlock(_ steps: Step<Action, State>...) -> [Step<Action, State>] {
18+
steps
19+
}
20+
21+
public static func buildEither(first component: [Step<Action, State>]) -> [Step<Action, State>] {
22+
component
23+
}
24+
25+
public static func buildEither(second component: [Step<Action, State>]) -> [Step<Action, State>] {
26+
component
27+
}
28+
29+
public static func buildLimitedAvailability(_ component: [Step<Action, State>]) -> [Step<Action, State>] {
30+
component
31+
}
32+
33+
public static func buildOptional(_ component: [Step<Action, State>]?) -> [Step<Action, State>] {
34+
component ?? []
35+
}
36+
37+
public static func buildArray(_ components: [[Step<Action, State>]]) -> [Step<Action, State>] {
38+
components.flatMap { $0 }
39+
}
40+
41+
public static func buildExpression<S: StepProtocol>(_ expression: S) -> Step<Action, State> where S.ActionType == Action, S.StateType == State {
42+
expression.asStep
43+
}
44+
}
45+
46+
public protocol StepProtocol {
47+
associatedtype ActionType
48+
associatedtype StateType
49+
50+
var asStep: Step<ActionType, StateType> { get }
51+
}
52+
53+
public struct Send<ActionType, StateType>: StepProtocol {
1754
public init(
1855
action: @autoclosure @escaping () -> ActionType,
1956
file: StaticString = #file,
@@ -30,9 +67,13 @@ public struct SendStep<ActionType, StateType> {
3067
let file: StaticString
3168
let line: UInt
3269
let stateChange: (inout StateType) -> Void
70+
71+
public var asStep: Step<ActionType, StateType> {
72+
.send(action: action(), file: file, line: line, stateChange: stateChange)
73+
}
3374
}
3475

35-
public struct ReceiveStep<ActionType, StateType> {
76+
public struct Receive<ActionType, StateType>: StepProtocol {
3677
public init(isExpectedAction: @escaping (ActionType) -> Bool,
3778
file: StaticString = #file,
3879
line: UInt = #line,
@@ -59,33 +100,60 @@ public struct ReceiveStep<ActionType, StateType> {
59100
let file: StaticString
60101
let line: UInt
61102
let stateChange: (inout StateType) -> Void
62-
63103
let isExpectedAction: (ActionType) -> Bool
104+
105+
public var asStep: Step<ActionType, StateType> {
106+
.receive(isExpectedAction: isExpectedAction, file: file, line: line, stateChange: stateChange)
107+
}
108+
}
109+
110+
public struct SideEffectResult<ActionType, StateType>: StepProtocol {
111+
public init(do perform: @escaping () -> Void) {
112+
self.perform = perform
113+
}
114+
let perform: () -> Void
115+
116+
public var asStep: Step<ActionType, StateType> {
117+
.sideEffectResult(do: perform)
118+
}
64119
}
65120

66-
public enum Step<ActionType, StateType> {
67-
case send(SendStep<ActionType, StateType>)
68-
case receive(ReceiveStep<ActionType, StateType>)
121+
public enum Step<ActionType, StateType>: StepProtocol {
122+
case send(
123+
action: @autoclosure () -> ActionType,
124+
file: StaticString = #file,
125+
line: UInt = #line,
126+
stateChange: (inout StateType) -> Void = { _ in }
127+
)
128+
case receive(
129+
isExpectedAction: (ActionType) -> Bool,
130+
file: StaticString = #file,
131+
line: UInt = #line,
132+
stateChange: (inout StateType) -> Void = { _ in }
133+
)
69134
case sideEffectResult(
70135
do: () -> Void
71136
)
137+
138+
public var asStep: Step<ActionType, StateType> {
139+
self
140+
}
72141
}
73142

74143
extension XCTestCase {
75144
public func assert<M: Middleware>(
76145
initialValue: M.StateType,
77146
reducer: Reducer<M.InputActionType, M.StateType>,
78147
middleware: M,
79-
steps: Step<M.InputActionType, M.StateType>...,
80-
otherSteps: [Step<M.InputActionType, M.StateType>] = [],
148+
@StepBuilder<M.InputActionType, M.StateType> steps: () -> [Step<M.InputActionType, M.StateType>],
81149
file: StaticString = #file,
82150
line: UInt = #line
83151
) where M.InputActionType == M.OutputActionType, M.StateType: Equatable {
84152
assert(
85153
initialValue: initialValue,
86154
reducer: reducer,
87155
middleware: middleware,
88-
steps: steps + otherSteps,
156+
steps: steps,
89157
stateEquating: ==,
90158
file: file,
91159
line: line
@@ -96,28 +164,7 @@ extension XCTestCase {
96164
initialValue: M.StateType,
97165
reducer: Reducer<M.InputActionType, M.StateType>,
98166
middleware: M,
99-
steps: Step<M.InputActionType, M.StateType>...,
100-
otherSteps: [Step<M.InputActionType, M.StateType>] = [],
101-
stateEquating: (M.StateType, M.StateType) -> Bool,
102-
file: StaticString = #file,
103-
line: UInt = #line
104-
) where M.InputActionType == M.OutputActionType {
105-
assert(
106-
initialValue: initialValue,
107-
reducer: reducer,
108-
middleware: middleware,
109-
steps: steps + otherSteps,
110-
stateEquating: stateEquating,
111-
file: file,
112-
line: line
113-
)
114-
}
115-
116-
public func assert<M: Middleware>(
117-
initialValue: M.StateType,
118-
reducer: Reducer<M.InputActionType, M.StateType>,
119-
middleware: M,
120-
steps: [Step<M.InputActionType, M.StateType>],
167+
@StepBuilder<M.InputActionType, M.StateType> steps: () -> [Step<M.InputActionType, M.StateType>],
121168
stateEquating: (M.StateType, M.StateType) -> Bool,
122169
file: StaticString = #file,
123170
line: UInt = #line
@@ -132,21 +179,17 @@ extension XCTestCase {
132179
}
133180
middleware.receiveContext(getState: { state }, output: anyActionHandler)
134181

135-
steps.forEach { outerStep in
182+
steps().forEach { outerStep in
136183
var expected = state
137184

138185
switch outerStep {
139-
case let .send(step)://action, file, line, stateChange):
140-
let file = step.file
141-
let line = step.line
142-
let stateChange = step.stateChange
143-
186+
case let .send(action, file, line, stateChange)://action, file, line, stateChange):
144187
if !middlewareResponses.isEmpty {
145188
XCTFail("Action sent before handling \(middlewareResponses.count) pending effect(s)", file: file, line: line)
146189
}
147190

148191
var afterReducer: AfterReducer = .doNothing()
149-
let action = step.action()
192+
let action = action()
150193
middleware.handle(
151194
action: action,
152195
from: .init(file: "\(file)", function: "", line: line, info: nil),
@@ -157,11 +200,7 @@ extension XCTestCase {
157200

158201
stateChange(&expected)
159202
ensureStateMutation(equating: stateEquating, statusQuo: state, expected: expected, step: outerStep)
160-
case let .receive(step)://action, file, line, stateChange):
161-
let file = step.file
162-
let line = step.line
163-
let stateChange = step.stateChange
164-
203+
case let .receive(action, file, line, stateChange)://action, file, line, stateChange):
165204
if middlewareResponses.isEmpty {
166205
_ = XCTWaiter.wait(for: [gotAction], timeout: 0.2)
167206
}
@@ -170,7 +209,7 @@ extension XCTestCase {
170209
break
171210
}
172211
let first = middlewareResponses.removeFirst()
173-
XCTAssertTrue(step.isExpectedAction(first), file: file, line: line)
212+
XCTAssertTrue(action(first), file: file, line: line)
174213

175214
var afterReducer: AfterReducer = .doNothing()
176215
middleware.handle(
@@ -208,9 +247,7 @@ extension XCTestCase {
208247
var stateString: String = "", expectedString: String = ""
209248
dump(statusQuo, to: &stateString, name: nil, indent: 2)
210249
dump(expected, to: &expectedString, name: nil, indent: 2)
211-
let difference = diff(old: expectedString, new: stateString) ?? ""
212-
213-
return "Expected state after step \(step) different from current state\n\(difference)"
250+
return "Expected state after step \(step) different from current state\n\(expectedString)\n\(stateString)"
214251
}(),
215252
file: file,
216253
line: line

0 commit comments

Comments
 (0)