Skip to content

Commit

Permalink
Added ExpectedSideEffect to WorkflowTesting
Browse files Browse the repository at this point in the history
  • Loading branch information
dhavalshreyas committed Jun 11, 2020
1 parent 857407c commit 0fa0ade
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 13 deletions.
1 change: 1 addition & 0 deletions swift/Workflow/Sources/SubtreeManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,7 @@ extension WorkflowNode.SubtreeManager {
}

deinit {
// Explicitly end the lifetime in case someone retained it from outside
lifetime.end()
}
}
Expand Down
41 changes: 40 additions & 1 deletion swift/Workflow/Tests/SubtreeManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,9 @@ final class SubtreeManagerTests: XCTestCase {
let sideEffectKey = manager.sideEffectLifetimes.values.first!

_ = manager.render { context -> TestViewModel in
context.runSideEffect(key: "helloWorld") { _ in }
context.runSideEffect(key: "helloWorld") { _ in
XCTFail("Unexpected SideEffect execution")
}
return context.render(
workflow: TestWorkflow(),
key: "",
Expand Down Expand Up @@ -361,6 +363,43 @@ final class SubtreeManagerTests: XCTestCase {
XCTAssertEqual(manager.sideEffectLifetimes.count, 0)
wait(for: [lifetimeEndedExpectation], timeout: 1)
}

func test_verifySideEffectsWithDifferentKeysAreExecuted() {
let manager = WorkflowNode<ParentWorkflow>.SubtreeManager()
XCTAssertTrue(manager.sideEffectLifetimes.isEmpty)

let firstSideEffectExecutedExpectation = expectation(description: "FirstSideEffect")
_ = manager.render { context -> TestViewModel in
context.runSideEffect(key: "key-1") { _ in
firstSideEffectExecutedExpectation.fulfill()
}
return context.render(
workflow: TestWorkflow(),
key: "",
outputMap: { _ in AnyWorkflowAction.noAction }
)
}

wait(for: [firstSideEffectExecutedExpectation], timeout: 1)
XCTAssertEqual(manager.sideEffectLifetimes.count, 1)
XCTAssertEqual(manager.sideEffectLifetimes.keys.first, "key-1")

let secondSideEffectExecutedExpectation = expectation(description: "SecondSideEffect")
_ = manager.render { context -> TestViewModel in
context.runSideEffect(key: "key-2") { _ in
secondSideEffectExecutedExpectation.fulfill()
}
return context.render(
workflow: TestWorkflow(),
key: "",
outputMap: { _ in AnyWorkflowAction.noAction }
)
}

wait(for: [secondSideEffectExecutedExpectation], timeout: 1)
XCTAssertEqual(manager.sideEffectLifetimes.count, 1)
XCTAssertEqual(manager.sideEffectLifetimes.keys.first, "key-2")
}
}

private struct TestViewModel {
Expand Down
25 changes: 24 additions & 1 deletion swift/WorkflowTesting/Sources/RenderExpectations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,22 @@ public struct RenderExpectations<WorkflowType: Workflow> {
var expectedOutput: ExpectedOutput<WorkflowType>?
var expectedWorkers: [ExpectedWorker]
var expectedWorkflows: [ExpectedWorkflow]
var expectedSideEffects: [AnyHashable: ExpectedSideEffect<WorkflowType>]

public init(
expectedState: ExpectedState<WorkflowType>? = nil,
expectedOutput: ExpectedOutput<WorkflowType>? = nil,
expectedWorkers: [ExpectedWorker] = [],
expectedWorkflows: [ExpectedWorkflow] = []
expectedWorkflows: [ExpectedWorkflow] = [],
expectedSideEffects: [ExpectedSideEffect<WorkflowType>] = []
) {
self.expectedState = expectedState
self.expectedOutput = expectedOutput
self.expectedWorkers = expectedWorkers
self.expectedWorkflows = expectedWorkflows
self.expectedSideEffects = expectedSideEffects.reduce(into: [AnyHashable: ExpectedSideEffect<WorkflowType>]()) { res, expectedSideEffect in
res[expectedSideEffect.key] = expectedSideEffect
}
}
}

Expand Down Expand Up @@ -99,6 +104,24 @@ public struct ExpectedWorker {
}
}

public struct ExpectedSideEffect<WorkflowType: Workflow> {
let key: AnyHashable
let action: ((RenderContext<WorkflowType>) -> Void)?
}

extension ExpectedSideEffect {
public init(key: AnyHashable) {
self.init(key: key) { _ in }
}

public init<ActionType: WorkflowAction>(key: AnyHashable, action: ActionType) where ActionType.WorkflowType == WorkflowType {
self.init(key: key) { context in
let sink = context.makeSink(of: ActionType.self)
sink.send(action)
}
}
}

public struct ExpectedWorkflow {
let workflowType: Any.Type
let key: String
Expand Down
30 changes: 24 additions & 6 deletions swift/WorkflowTesting/Sources/WorkflowRenderTester.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
@testable import Workflow

import ReactiveSwift
import class Workflow.Lifetime
import XCTest

extension Workflow {
Expand Down Expand Up @@ -207,13 +208,15 @@
expectedOutput: ExpectedOutput<WorkflowType>? = nil,
expectedWorkers: [ExpectedWorker] = [],
expectedWorkflows: [ExpectedWorkflow] = [],
expectedSideEffects: [ExpectedSideEffect<WorkflowType>] = [],
assertions: (WorkflowType.Rendering) -> Void
) -> RenderTester<WorkflowType> {
let expectations = RenderExpectations(
expectedState: expectedState,
expectedOutput: expectedOutput,
expectedWorkers: expectedWorkers,
expectedWorkflows: expectedWorkflows
expectedWorkflows: expectedWorkflows,
expectedSideEffects: expectedSideEffects
)

return render(file: file, line: line, with: expectations, assertions: assertions)
Expand All @@ -230,7 +233,7 @@
fileprivate final class RenderTestContext<T: Workflow>: RenderContextType {
typealias WorkflowType = T

private var (lifetime, token) = Lifetime.make()
private var (lifetime, token) = ReactiveSwift.Lifetime.make()

var state: WorkflowType.State
var expectations: RenderExpectations<WorkflowType>
Expand All @@ -247,7 +250,7 @@
func render<Child, Action>(workflow: Child, key: String, outputMap: @escaping (Child.Output) -> Action) -> Child.Rendering where Child: Workflow, Action: WorkflowAction, RenderTestContext<T>.WorkflowType == Action.WorkflowType {
guard let workflowIndex = expectations.expectedWorkflows.firstIndex(where: { expectedWorkflow -> Bool in
type(of: workflow) == expectedWorkflow.workflowType && key == expectedWorkflow.key
}) else {
}) else {
XCTFail("Unexpected child workflow of type \(workflow.self)", file: file, line: line)
fatalError()
}
Expand Down Expand Up @@ -277,7 +280,7 @@
func awaitResult<W, Action>(for worker: W, outputMap: @escaping (W.Output) -> Action) where W: Worker, Action: WorkflowAction, RenderTestContext<T>.WorkflowType == Action.WorkflowType {
guard let workerIndex = expectations.expectedWorkers.firstIndex(where: { (expectedWorker) -> Bool in
expectedWorker.isEquivalent(to: worker)
}) else {
}) else {
XCTFail("Unexpected worker during render \(worker)", file: file, line: line)
return
}
Expand All @@ -288,6 +291,15 @@
}
}

func runSideEffect(key: AnyHashable, action: (Lifetime) -> Void) {
guard let sideEffect = expectations.expectedSideEffects.removeValue(forKey: key) else {
XCTFail("Unexpected side-effect during render \(key)", file: file, line: line)
return
}

sideEffect.action?(RenderContext.make(implementation: self))
}

private func apply<Action>(action: Action) where Action: WorkflowAction, Action.WorkflowType == WorkflowType {
let output = action.apply(toState: &state)
switch (output, expectations.expectedOutput) {
Expand Down Expand Up @@ -317,17 +329,23 @@
XCTFail("Expected output of '\(outputExpectation.output)' but received none.", file: file, line: line)
}

if expectations.expectedWorkers.count != 0 {
if !expectations.expectedWorkers.isEmpty {
for expectedWorker in expectations.expectedWorkers {
XCTFail("Expected worker \(expectedWorker.worker)", file: file, line: line)
}
}

if expectations.expectedWorkflows.count != 0 {
if !expectations.expectedWorkflows.isEmpty {
for expectedWorkflow in expectations.expectedWorkflows {
XCTFail("Expected child workflow of type: \(expectedWorkflow.workflowType) key: \(expectedWorkflow.key)", file: file, line: line)
}
}

if !expectations.expectedSideEffects.isEmpty {
for expectedSideEffect in expectations.expectedSideEffects {
XCTFail("Expected side-effect with key: \(expectedSideEffect.key)", file: file, line: line)
}
}
}
}

Expand Down
68 changes: 63 additions & 5 deletions swift/WorkflowTesting/Tests/WorkflowRenderTesterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ final class WorkflowRenderTesterTests: XCTestCase {
state: TestWorkflow.State(
text: "initial",
substate: .idle
))),
)
)
),
assertions: { screen in
XCTAssertEqual("initial", screen.text)
testedAssertion = true
Expand All @@ -68,20 +70,37 @@ final class WorkflowRenderTesterTests: XCTestCase {
state: TestWorkflow.State(
text: "initial",
substate: .waiting
))),
)
)
),
assertions: { screen in
XCTAssertEqual("initial", screen.text)
screen.tapped()
}
)
}

func test_sideEffects() {
let renderTester = SideEffectWorkflow().renderTester()

renderTester.render(
with: RenderExpectations(
expectedState: ExpectedState(state: .success),
expectedSideEffects: [
ExpectedSideEffect(key: TestSideEffectKey(), action: SideEffectWorkflow.Action.testAction),
]
),
assertions: { _ in }
)
}

func test_output() {
OutputWorkflow()
.renderTester()
.render(
with: RenderExpectations(
expectedOutput: ExpectedOutput(output: .success)),
expectedOutput: ExpectedOutput(output: .success)
),
assertions: { rendering in
rendering.tapped()
}
Expand All @@ -94,7 +113,8 @@ final class WorkflowRenderTesterTests: XCTestCase {
initialState: TestWorkflow.State(
text: "otherText",
substate: .waiting
))
)
)

let expectedWorker = ExpectedWorker(worker: TestWorker(text: "otherText"))

Expand Down Expand Up @@ -196,7 +216,8 @@ final class WorkflowRenderTesterTests: XCTestCase {
state: TestWorkflow.State(
text: "hello",
substate: .idle
)),
)
),
expectedOutput: nil,
expectedWorkers: [],
expectedWorkflows: [],
Expand Down Expand Up @@ -306,6 +327,43 @@ private struct OutputWorkflow: Workflow {
}
}

private struct TestSideEffectKey: Hashable {
let key: String = "Test Side Effect"
}

private struct SideEffectWorkflow: Workflow {
enum State: Equatable {
case idle
case success
}

enum Action: WorkflowAction {
case testAction

typealias WorkflowType = SideEffectWorkflow

func apply(toState state: inout SideEffectWorkflow.State) -> SideEffectWorkflow.Output? {
switch self {
case .testAction:
state = .success
}
return nil
}
}

typealias Rendering = TestScreen

func render(state: State, context: RenderContext<SideEffectWorkflow>) -> TestScreen {
context.runSideEffect(key: TestSideEffectKey()) { _ in }

return TestScreen(text: "value", tapped: {})
}

func makeInitialState() -> State {
.idle
}
}

private struct TestWorker: Worker {
var text: String

Expand Down

0 comments on commit 0fa0ade

Please sign in to comment.