Skip to content

Commit 3ac5ed9

Browse files
authored
Merge pull request #4 from SwiftRex/TestingDSL
DSL for SwiftRex functional tests
2 parents c162589 + 8aa7ecf commit 3ac5ed9

File tree

2 files changed

+231
-48
lines changed

2 files changed

+231
-48
lines changed

README.md

Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,134 @@
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)).expectStateToHaveChanged { state in
37+
// if during Send or Receive action, your state is expected to mutate, you must indicate which change is expected to happen here:
38+
state.somePropertyShouldHaveChangedTo = true
39+
// any unexpected state mutation will fail the test, as well as any expected state mutation that doesn't occur, will also fail the test
40+
}
41+
42+
SideEffectResult {
43+
scheduler.advance(by: .seconds(5))
44+
}
45+
46+
// if you receive an action and don't add this block, the test will fail to remind you
47+
Receive { action -> Bool in
48+
// validates that the received action is what you would expect
49+
// if this function returns false, the test will fail to show you that you've got an unexpected action
50+
if case let .my(.expectedAction(list)) = action {
51+
return list.isEmpty
52+
} else {
53+
return false
54+
}
55+
}.expectStateToHaveChanged { state in
56+
// if during Send or Receive action, your state is expected to mutate, you must indicate which change is expected to happen here:
57+
state.somePropertyShouldHaveChangedTo = true
58+
// any unexpected state mutation will fail the test, as well as any expected state mutation that doesn't occur, will also fail the test
59+
}
60+
}
61+
)
62+
```
63+
64+
## Combine
65+
66+
Validate Output of Publishers
67+
```swift
68+
let operation = assert(
69+
publisher: myPublisher,
70+
eventuallyReceives: "🙉", "🙊", "🙈",
71+
andCompletes: false
72+
)
73+
somethingHappensAndPublisherReceives("🙉")
74+
somethingHappensAndPublisherReceives("🙊")
75+
somethingHappensAndPublisherReceives("🙈")
76+
77+
operation.wait(0.0001)
78+
```
79+
80+
Validate Output and Successful Completion of Publishers
81+
```swift
82+
let operation = assert(
83+
publisher: myPublisher,
84+
eventuallyReceives: "🙉", "🙊", "🙈",
85+
andCompletesWith: .isSuccess
86+
)
87+
somethingHappensAndPublisherReceives("🙉")
88+
somethingHappensAndPublisherReceives("🙊")
89+
somethingHappensAndPublisherReceives("🙈")
90+
somethingHappensAndPublisherCompletes()
91+
92+
operation.wait(0.0001)
93+
```
94+
95+
Validate Output and some Failure of Publishers
96+
```swift
97+
let operation = assert(
98+
publisher: myPublisher,
99+
eventuallyReceives: "🙉", "🙊", "🙈",
100+
andCompletesWith: .isFailure
101+
)
102+
somethingHappensAndPublisherReceives("🙉")
103+
somethingHappensAndPublisherReceives("🙊")
104+
somethingHappensAndPublisherReceives("🙈")
105+
somethingHappensAndPublisherFails(SomeError())
106+
107+
operation.wait(0.0001)
108+
```
109+
110+
Validate Output and specific Failure of Publishers
111+
```swift
112+
let operation = assert(
113+
publisher: myPublisher,
114+
eventuallyReceives: "🙉", "🙊", "🙈",
115+
andCompletesWith: .failedWithError { error in error == SomeError("123") }
116+
)
117+
somethingHappensAndPublisherReceives("🙉")
118+
somethingHappensAndPublisherReceives("🙊")
119+
somethingHappensAndPublisherReceives("🙈")
120+
somethingHappensAndPublisherFails(SomeError("123"))
121+
122+
operation.wait(0.0001)
123+
```
124+
125+
Validate No Output but completion of Publishers
126+
```swift
127+
let operation = assert(
128+
publisher: myPublisher,
129+
completesWithoutValues: .isSuccess
130+
)
131+
somethingHappensAndPublisherCompletes()
132+
133+
operation.wait(0.0001)
134+
```

Sources/TestingExtensions/UseCaseTests.swift

Lines changed: 98 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,20 @@ 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+
}
74+
75+
public func expectStateToHaveChanged(_ expectedMutation: @escaping (inout StateType) -> Void = { _ in }) -> Send {
76+
.init(action: action(), file: file, line: line, stateChange: { state in
77+
self.stateChange(&state)
78+
expectedMutation(&state)
79+
})
80+
}
3381
}
3482

35-
public struct ReceiveStep<ActionType, StateType> {
83+
public struct Receive<ActionType, StateType>: StepProtocol {
3684
public init(isExpectedAction: @escaping (ActionType) -> Bool,
3785
file: StaticString = #file,
3886
line: UInt = #line,
@@ -59,33 +107,67 @@ public struct ReceiveStep<ActionType, StateType> {
59107
let file: StaticString
60108
let line: UInt
61109
let stateChange: (inout StateType) -> Void
62-
63110
let isExpectedAction: (ActionType) -> Bool
111+
112+
public var asStep: Step<ActionType, StateType> {
113+
.receive(isExpectedAction: isExpectedAction, file: file, line: line, stateChange: stateChange)
114+
}
115+
116+
public func expectStateToHaveChanged(_ expectedMutation: @escaping (inout StateType) -> Void = { _ in }) -> Receive {
117+
.init(isExpectedAction: isExpectedAction, file: file, line: line, stateChange: { state in
118+
self.stateChange(&state)
119+
expectedMutation(&state)
120+
})
121+
}
64122
}
65123

66-
public enum Step<ActionType, StateType> {
67-
case send(SendStep<ActionType, StateType>)
68-
case receive(ReceiveStep<ActionType, StateType>)
124+
public struct SideEffectResult<ActionType, StateType>: StepProtocol {
125+
public init(do perform: @escaping () -> Void) {
126+
self.perform = perform
127+
}
128+
let perform: () -> Void
129+
130+
public var asStep: Step<ActionType, StateType> {
131+
.sideEffectResult(do: perform)
132+
}
133+
}
134+
135+
public enum Step<ActionType, StateType>: StepProtocol {
136+
case send(
137+
action: @autoclosure () -> ActionType,
138+
file: StaticString = #file,
139+
line: UInt = #line,
140+
stateChange: (inout StateType) -> Void = { _ in }
141+
)
142+
case receive(
143+
isExpectedAction: (ActionType) -> Bool,
144+
file: StaticString = #file,
145+
line: UInt = #line,
146+
stateChange: (inout StateType) -> Void = { _ in }
147+
)
69148
case sideEffectResult(
70149
do: () -> Void
71150
)
151+
152+
public var asStep: Step<ActionType, StateType> {
153+
self
154+
}
72155
}
73156

74157
extension XCTestCase {
75158
public func assert<M: Middleware>(
76159
initialValue: M.StateType,
77160
reducer: Reducer<M.InputActionType, M.StateType>,
78161
middleware: M,
79-
steps: Step<M.InputActionType, M.StateType>...,
80-
otherSteps: [Step<M.InputActionType, M.StateType>] = [],
162+
@StepBuilder<M.InputActionType, M.StateType> steps: () -> [Step<M.InputActionType, M.StateType>],
81163
file: StaticString = #file,
82164
line: UInt = #line
83165
) where M.InputActionType == M.OutputActionType, M.StateType: Equatable {
84166
assert(
85167
initialValue: initialValue,
86168
reducer: reducer,
87169
middleware: middleware,
88-
steps: steps + otherSteps,
170+
steps: steps,
89171
stateEquating: ==,
90172
file: file,
91173
line: line
@@ -96,28 +178,7 @@ extension XCTestCase {
96178
initialValue: M.StateType,
97179
reducer: Reducer<M.InputActionType, M.StateType>,
98180
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>],
181+
@StepBuilder<M.InputActionType, M.StateType> steps: () -> [Step<M.InputActionType, M.StateType>],
121182
stateEquating: (M.StateType, M.StateType) -> Bool,
122183
file: StaticString = #file,
123184
line: UInt = #line
@@ -132,21 +193,17 @@ extension XCTestCase {
132193
}
133194
middleware.receiveContext(getState: { state }, output: anyActionHandler)
134195

135-
steps.forEach { outerStep in
196+
steps().forEach { outerStep in
136197
var expected = state
137198

138199
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-
200+
case let .send(action, file, line, stateChange)://action, file, line, stateChange):
144201
if !middlewareResponses.isEmpty {
145202
XCTFail("Action sent before handling \(middlewareResponses.count) pending effect(s)", file: file, line: line)
146203
}
147204

148205
var afterReducer: AfterReducer = .doNothing()
149-
let action = step.action()
206+
let action = action()
150207
middleware.handle(
151208
action: action,
152209
from: .init(file: "\(file)", function: "", line: line, info: nil),
@@ -157,11 +214,7 @@ extension XCTestCase {
157214

158215
stateChange(&expected)
159216
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-
217+
case let .receive(action, file, line, stateChange)://action, file, line, stateChange):
165218
if middlewareResponses.isEmpty {
166219
_ = XCTWaiter.wait(for: [gotAction], timeout: 0.2)
167220
}
@@ -170,7 +223,7 @@ extension XCTestCase {
170223
break
171224
}
172225
let first = middlewareResponses.removeFirst()
173-
XCTAssertTrue(step.isExpectedAction(first), file: file, line: line)
226+
XCTAssertTrue(action(first), file: file, line: line)
174227

175228
var afterReducer: AfterReducer = .doNothing()
176229
middleware.handle(
@@ -208,9 +261,7 @@ extension XCTestCase {
208261
var stateString: String = "", expectedString: String = ""
209262
dump(statusQuo, to: &stateString, name: nil, indent: 2)
210263
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)"
264+
return "Expected state after step \(step) different from current state\n\(expectedString)\n\(stateString)"
214265
}(),
215266
file: file,
216267
line: line

0 commit comments

Comments
 (0)