From 4bcbd4417ed7c72bd44642a785f1b2ce95f0ae12 Mon Sep 17 00:00:00 2001 From: Alexander Voronov Date: Tue, 4 Sep 2018 11:43:39 +0300 Subject: [PATCH 1/7] corrected specs --- .../xcshareddata/IDEWorkspaceChecks.plist | 8 ++++++++ Example/Tests/Calls/SwiftyMockCallsSpec.swift | 4 ++-- .../Tests/Calls/SwiftyMockReactiveCallsSpec.swift | 12 ++++++------ 3 files changed, 16 insertions(+), 8 deletions(-) create mode 100644 Example/SwiftyMock.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/Example/SwiftyMock.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Example/SwiftyMock.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Example/SwiftyMock.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Example/Tests/Calls/SwiftyMockCallsSpec.swift b/Example/Tests/Calls/SwiftyMockCallsSpec.swift index 121b466..92d3d95 100644 --- a/Example/Tests/Calls/SwiftyMockCallsSpec.swift +++ b/Example/Tests/Calls/SwiftyMockCallsSpec.swift @@ -11,11 +11,11 @@ import Quick import Nimble @testable import SwiftyMock -protocol Calculator { +fileprivate protocol Calculator { func sum(left: Int, right: Int) -> Int } -class TestCalculator: Calculator { +fileprivate class TestCalculator: Calculator { let sum = FunctionCall<(left: Int, right: Int), Int>() @discardableResult func sum(left: Int, right: Int) -> Int { return stubCall(sum, argument: (left: left, right: right)) diff --git a/Example/Tests/Calls/SwiftyMockReactiveCallsSpec.swift b/Example/Tests/Calls/SwiftyMockReactiveCallsSpec.swift index d161e3f..4b3a426 100644 --- a/Example/Tests/Calls/SwiftyMockReactiveCallsSpec.swift +++ b/Example/Tests/Calls/SwiftyMockReactiveCallsSpec.swift @@ -13,11 +13,11 @@ import ReactiveSwift import Result @testable import SwiftyMock -protocol ReactiveCalculator { +fileprivate protocol ReactiveCalculator { func sum(left: Int, right: Int) -> SignalProducer } -class TestReactiveCalculator: ReactiveCalculator { +fileprivate class TestReactiveCalculator: ReactiveCalculator { init() {} let sum = ReactiveCall<(left: Int, right: Int), Int, TestError>() @@ -26,7 +26,7 @@ class TestReactiveCalculator: ReactiveCalculator { } } -struct TestError: Error, Equatable { +fileprivate struct TestError: Error, Equatable { let id: Int init() { id = 0 } init(id: Int) { self.id = id } @@ -57,7 +57,7 @@ class SwiftyMockReactiveCallsSpec: QuickSpec { } context("when calling method before stubbing") { - fit("should return empty signal without any value") { + it("should return empty signal without any value") { expect { sut.sum(left: 1,right: 2) }.to(complete()) } } @@ -224,8 +224,8 @@ class SwiftyMockReactiveCallsSpec: QuickSpec { context("when calling filtered stubbed with block method") { beforeEach { sut.sum.returns(.success(17)) - sut.sum.on { $0.left == 12 }.performs { .success($0.left - $0.right) } - sut.sum.on { $0.right == 42 }.performs { _ in .failure(TestError()) } + sut.sum.on { $0.left == 12 }.performs { .success($0.left - $0.right) } + sut.sum.on { $0.left == 42 }.performs { _ in .failure(TestError()) } } context("when parameters matching filter") { it("should return calculated with stub value") { From 5ee85796dc18ea73f9d94b59fb983f4e3e5093eb Mon Sep 17 00:00:00 2001 From: Alexander Voronov Date: Tue, 4 Sep 2018 11:44:06 +0300 Subject: [PATCH 2/7] added matchers for reactive events --- Example/Tests/Calls/ReactiveMatchers.swift | 147 +++++++++++++++++---- 1 file changed, 123 insertions(+), 24 deletions(-) diff --git a/Example/Tests/Calls/ReactiveMatchers.swift b/Example/Tests/Calls/ReactiveMatchers.swift index 573bc9c..8da059f 100644 --- a/Example/Tests/Calls/ReactiveMatchers.swift +++ b/Example/Tests/Calls/ReactiveMatchers.swift @@ -137,21 +137,12 @@ func sendEmptyValueAndComplete() -> Predicate w // MARK: - Complete func complete() -> Predicate where T.Value == V, T.Error == E { - return Predicate { (actualExpression: Expression) throws -> PredicateResult in - var actualEvent: Signal.Event? - var completed: Bool = false - let actualProducer = try actualExpression.evaluate() - actualProducer?.producer.start { event in - actualEvent = event - if case .completed = event { - completed = true - } - } - return PredicateResult( - bool: completed, - message: .expectedCustomValueTo("complete", message(forEvent: actualEvent)) + return sendEvent(where: { $0.isCompleted }, expectation: { actualEvent in + .expectedCustomValueTo( + "complete", + message(forEvent: actualEvent) ) - } + }) } // MARK: - Fail @@ -224,38 +215,146 @@ func failWithNoError() -> Predicate where T.Err // MARK: - Interrupt func interrupt() -> Predicate where T.Value == V, T.Error == E { + return sendEvent(where: { $0.isInterrupted }, expectation: { actualEvent in + .expectedCustomValueTo( + "interrupt", + message(forEvent: actualEvent) + ) + }) +} + +// MARK: - Event + +private func sendEvent( + where predicate: @escaping (Signal.Event) -> Bool, + expectation: @escaping (Signal.Event?) -> ExpectationMessage +) -> Predicate where T.Value == V, T.Error == E { return Predicate { (actualExpression: Expression) throws -> PredicateResult in var actualEvent: Signal.Event? - var interrupted: Bool = false + var satisfies: Bool = false let actualProducer = try actualExpression.evaluate() actualProducer?.producer.start { event in actualEvent = event - if case .interrupted = event { - interrupted = true - } + satisfies = predicate(event) } return PredicateResult( - bool: interrupted, - message: .expectedCustomValueTo("interrupt", message(forEvent: actualEvent)) + bool: satisfies, + message: expectation(actualEvent) + ) + } +} + +func sendEvent(where predicate: @escaping (Signal.Event) -> Bool) -> Predicate where T.Value == V, T.Error == E { + return sendEvent(where: predicate, expectation: { actualEvent in + .expectedCustomValueTo( + "send event to satisfy predicate", + message(forEvent: actualEvent) + ) + }) +} + +func sendEvent(_ expectedEvent: Signal.Event) -> Predicate where T.Value == V, T.Error == E { + return sendEvent(where: { $0 == expectedEvent }, expectation: { actualEvent in + .expectedCustomValueTo( + "send " + message(forEvent: expectedEvent), + message(forEvent: actualEvent) + ) + }) +} + +// MARK: - Events + +private func sendEvents( + where predicate: @escaping ([Signal.Event]) -> Bool, + expectation: @escaping ([Signal.Event]) -> ExpectationMessage +) -> Predicate where T.Value == V, T.Error == E { + return Predicate { (actualExpression: Expression) throws -> PredicateResult in + var actualEvents: [Signal.Event] = [] + var satisfies: Bool = false + let actualProducer = try actualExpression.evaluate() + actualProducer?.producer + .on(event: { event in + actualEvents.append(event) + }) + .on(terminated: { + satisfies = predicate(actualEvents) + }) + .start() + return PredicateResult( + bool: satisfies, + message: expectation(actualEvents) ) } } +func sendEvents(where predicate: @escaping ([Signal.Event]) -> Bool) -> Predicate where T.Value == V, T.Error == E { + return sendEvents(where: predicate, expectation: { actualEvents in + .expectedCustomValueTo( + "send events to satisfy predicate", + message(forEvents: actualEvents) + ) + }) +} + +func sendEvents(_ expectedEvents: [Signal.Event]) -> Predicate where T.Value == V, T.Error == E { + return sendEvents(where: { $0 == expectedEvents }, expectation: { actualEvents in + .expectedCustomValueTo( + "send " + message(forEvents: expectedEvents), + message(forEvents: actualEvents) + ) + }) +} + +func sendEvents(whereAll predicate: @escaping (Signal.Event) -> Bool) -> Predicate where T.Value == V, T.Error == E { + return sendEvents(where: { events in !events.contains(where: { !predicate($0) }) }, expectation: { actualEvents in + .expectedCustomValueTo( + "send all events to satisfy predicate", + message(forEvents: actualEvents) + ) + }) +} + +func sendEvents(whereAny predicate: @escaping (Signal.Event) -> Bool) -> Predicate where T.Value == V, T.Error == E { + return sendEvents(where: { events in events.contains(where: predicate) }, expectation: { actualEvents in + .expectedCustomValueTo( + "send at least one event to satisfy predicate", + message(forEvents: actualEvents) + ) + }) +} + // MARK: - Helpers -private func errorMatchesExpectedError(_ actualError: Error, expectedError: T) -> Bool { +fileprivate extension Signal.Event { + var isInterrupted: Bool { + if case .interrupted = event { return true } + return false + } +} + +extension Signal.Event: Equatable where Signal.Value: Equatable, Signal.Error: Equatable {} + +fileprivate func errorMatchesExpectedError(_ actualError: Error, expectedError: T) -> Bool { return actualError._domain == expectedError._domain && actualError._code == expectedError._code } -private func message(forEvent event: Signal.Event?) -> String { +fileprivate func message(forEvents events: [Signal.Event]?) -> String { + if let events = events { + let stringifiedEvents = events.map(stringify).joined(separator: ", ") + return "<[\(stringifiedEvents)]> events" + } + return "no events" +} + +fileprivate func message(forEvent event: Signal.Event?) -> String { if let event = event { return "<\(stringify(event))> event" } return "no event" } -private func message(forEvent event: Signal.Event?, value: V?) -> String { +fileprivate func message(forEvent event: Signal.Event?, value: V?) -> String { if let event = event { if case .value = event { return "<\(stringify(value))> value" @@ -265,7 +364,7 @@ private func message(forEvent event: Signal.Event?, value: V?) -> St return "no event" } -private func message(forEvent event: Signal.Event?, error: E?) -> String { +fileprivate func message(forEvent event: Signal.Event?, error: E?) -> String { if let event = event { if case .failed = event { return "<\(stringify(error))> error" From 83ffb69e7372f0c3fa93356e0a2ebde8f8480539 Mon Sep 17 00:00:00 2001 From: Alexander Voronov Date: Tue, 4 Sep 2018 11:45:15 +0300 Subject: [PATCH 3/7] added stub call for reactive events along with result --- .../ReactiveCocoa/ReactiveExtensions.swift | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/SwiftyMock/Classes/ReactiveCocoa/ReactiveExtensions.swift b/SwiftyMock/Classes/ReactiveCocoa/ReactiveExtensions.swift index 5ac3479..ec5c595 100644 --- a/SwiftyMock/Classes/ReactiveCocoa/ReactiveExtensions.swift +++ b/SwiftyMock/Classes/ReactiveCocoa/ReactiveExtensions.swift @@ -13,6 +13,9 @@ import Result public typealias ReactiveCall = FunctionCall> public typealias ReactiveVoidCall = FunctionVoidCall> +public typealias ReactiveEventsCall = FunctionCall.Event]> +public typealias ReactiveEventsVoidCall = FunctionVoidCall<[Signal.Event]> + // Stub Signal Producer Call public func stubCall(_ call: ReactiveCall, argument: Arg, defaultValue: Result? = nil) -> SignalProducer { @@ -30,12 +33,63 @@ public func stubCall(_ call: ReactiveCall, arg return SignalProducer(result: result) } +public func stubCall(_ call: ReactiveEventsCall, argument: Arg, defaultEvents: [Signal.Event] = []) -> SignalProducer { + + // returning empty signal producer if no default value provide, thus preventing failure assert + if call.stubbedBlocks.isEmpty && call.stubbedBlock == nil && call.stubbedValue == nil && defaultEvents.isEmpty { + call.capture(argument) + return .empty + } + + // otherwise - duplicate normal function stubbing flow + call.capture(argument) + + // and returning signal producer by sending all events + return SignalProducer { (observer, lifetime) in + // we're sending completed event in case array of events doesn't contain one of event that completes signal: (interrupted | completed | failed) + // if there's one of such events, observer won't send another one completing event, thus nothing will happen + defer { observer.sendCompleted() } + + for stub in call.stubbedBlocks { + if stub.filter(argument) { + if case let .some(stubbedBlock) = stub.stubbedBlock { + return stubbedBlock(argument).forEach(observer.send) + } + + if case let .some(stubbedValue) = stub.stubbedValue { + return stubbedValue.forEach(observer.send) + } + } + } + + if case let .some(stubbedBlock) = call.stubbedBlock { + return stubbedBlock(argument).forEach(observer.send) + } + + if case let .some(stubbedValue) = call.stubbedValue { + return stubbedValue.forEach(observer.send) + } + + if !defaultEvents.isEmpty { + return defaultEvents.forEach(observer.send) + } + + assertionFailure("stub doesnt' have events to send") + + return call.stubbedValue!.forEach(observer.send) + } +} + // MARK: - Function Call Mock/Stub/Spy Without Arguments public func stubCall(_ call: ReactiveVoidCall, defaultValue: Result? = nil) -> SignalProducer { return stubCall(call, argument: (), defaultValue: defaultValue) } +public func stubCall(_ call: ReactiveEventsVoidCall, defaultEvents: [Signal.Event] = []) -> SignalProducer { + return stubCall(call, argument: (), defaultEvents: defaultEvents) +} + // MARK: - Stub Action Call public func stubCall(_ call: ReactiveVoidCall, defaultValue: Result? = nil) -> Action { From 4ce00555c9c4000c5dea01be92942bb4800fbaa4 Mon Sep 17 00:00:00 2001 From: Alexander Voronov Date: Tue, 4 Sep 2018 11:46:00 +0300 Subject: [PATCH 4/7] extended mock utils to accept single element if value is array --- SwiftyMock/Classes/Core/MockUtils.swift | 30 +++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/SwiftyMock/Classes/Core/MockUtils.swift b/SwiftyMock/Classes/Core/MockUtils.swift index b50f629..78a86c9 100644 --- a/SwiftyMock/Classes/Core/MockUtils.swift +++ b/SwiftyMock/Classes/Core/MockUtils.swift @@ -47,6 +47,18 @@ open class FunctionCall { } } +public extension FunctionCall { + public func returns(_ element: E) where Value == Array { + returns([element]) + } + + public func performs(_ block: @escaping (Arg) -> E) where Value == Array { + performs({ arg in [block(arg)] }) + } +} + +// MARK: - Stub Call + public func stubCall(_ call: FunctionCall, argument: Arg, defaultValue: Value? = nil) -> Value { call.capture(argument) @@ -89,17 +101,31 @@ open class ReturnContext { self.stub = stub } - @discardableResult open func returns(_ value: Value) -> FunctionCall { + @discardableResult + open func returns(_ value: Value) -> FunctionCall { stub.stubbedValue = value return call } - @discardableResult open func performs(_ block: @escaping ((Arg) -> Value)) -> FunctionCall { + @discardableResult + open func performs(_ block: @escaping ((Arg) -> Value)) -> FunctionCall { stub.stubbedBlock = block return call } } +public extension ReturnContext { + @discardableResult + public func returns(_ element: E) -> FunctionCall where Value == Array { + return returns([element]) + } + + @discardableResult + public func performs(_ block: @escaping ((Arg) -> E)) -> FunctionCall where Value == Array { + return performs({ arg in [block(arg)] }) + } +} + open class ReturnStub { let filter: (Arg) -> Bool fileprivate(set) var stubbedValue: Value? From 9990b2d594e00ae0079f76b5e1b11cd27186b729 Mon Sep 17 00:00:00 2001 From: Alexander Voronov Date: Tue, 4 Sep 2018 11:46:44 +0300 Subject: [PATCH 5/7] added specs for reactive events call --- Example/SwiftyMock.xcodeproj/project.pbxproj | 4 + .../SwiftyMockReactiveEventsCallsSpec.swift | 248 ++++++++++++++++++ 2 files changed, 252 insertions(+) create mode 100644 Example/Tests/Calls/SwiftyMockReactiveEventsCallsSpec.swift diff --git a/Example/SwiftyMock.xcodeproj/project.pbxproj b/Example/SwiftyMock.xcodeproj/project.pbxproj index 23bd88c..d43b020 100644 --- a/Example/SwiftyMock.xcodeproj/project.pbxproj +++ b/Example/SwiftyMock.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ D21AF4631FC62DF600C0DC5F /* SwiftyMockReactiveCallsSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21AF4611FC62CFF00C0DC5F /* SwiftyMockReactiveCallsSpec.swift */; }; D21AF4661FC638F200C0DC5F /* ReactiveMatchers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21AF4641FC6324400C0DC5F /* ReactiveMatchers.swift */; }; D2AEE57F20F9559000FF7DC8 /* Mock.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2AEE57E20F9559000FF7DC8 /* Mock.generated.swift */; }; + D2E357F5213DE087009417DA /* SwiftyMockReactiveEventsCallsSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2E357F4213DE087009417DA /* SwiftyMockReactiveEventsCallsSpec.swift */; }; E434BE281D4AAE86000E7125 /* SwiftyMockCallsSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = E434BE271D4AAE86000E7125 /* SwiftyMockCallsSpec.swift */; }; E481D6901D4DDE61000E73AE /* RoboKittenControllerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = E481D68F1D4DDE61000E73AE /* RoboKittenControllerSpec.swift */; }; EEDBE358155D115C15126670 /* RoboKitten.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEDBEBCC4DF10B0C24246892 /* RoboKitten.swift */; }; @@ -63,6 +64,7 @@ D21AF4611FC62CFF00C0DC5F /* SwiftyMockReactiveCallsSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyMockReactiveCallsSpec.swift; sourceTree = ""; }; D21AF4641FC6324400C0DC5F /* ReactiveMatchers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactiveMatchers.swift; sourceTree = ""; }; D2AEE57E20F9559000FF7DC8 /* Mock.generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Mock.generated.swift; sourceTree = ""; }; + D2E357F4213DE087009417DA /* SwiftyMockReactiveEventsCallsSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyMockReactiveEventsCallsSpec.swift; sourceTree = ""; }; E434BE271D4AAE86000E7125 /* SwiftyMockCallsSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftyMockCallsSpec.swift; sourceTree = ""; }; E481D6831D4DDDBE000E73AE /* RoboKittenTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RoboKittenTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; E481D6871D4DDDBE000E73AE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -213,6 +215,7 @@ children = ( E434BE271D4AAE86000E7125 /* SwiftyMockCallsSpec.swift */, D21AF4611FC62CFF00C0DC5F /* SwiftyMockReactiveCallsSpec.swift */, + D2E357F4213DE087009417DA /* SwiftyMockReactiveEventsCallsSpec.swift */, D21AF4641FC6324400C0DC5F /* ReactiveMatchers.swift */, ); path = Calls; @@ -542,6 +545,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D2E357F5213DE087009417DA /* SwiftyMockReactiveEventsCallsSpec.swift in Sources */, E434BE281D4AAE86000E7125 /* SwiftyMockCallsSpec.swift in Sources */, D21AF4661FC638F200C0DC5F /* ReactiveMatchers.swift in Sources */, D21AF4631FC62DF600C0DC5F /* SwiftyMockReactiveCallsSpec.swift in Sources */, diff --git a/Example/Tests/Calls/SwiftyMockReactiveEventsCallsSpec.swift b/Example/Tests/Calls/SwiftyMockReactiveEventsCallsSpec.swift new file mode 100644 index 0000000..d27e779 --- /dev/null +++ b/Example/Tests/Calls/SwiftyMockReactiveEventsCallsSpec.swift @@ -0,0 +1,248 @@ +// +// SwiftyMockReactiveEventsCallsSpec.swift +// SwiftyMock_Tests +// +// Created by Alexander Voronov on 9/4/18. +// Copyright © 2018 CocoaPods. All rights reserved. +// + +import Foundation +import Quick +import Nimble +import ReactiveSwift +import Result +@testable import SwiftyMock + +fileprivate protocol ReactiveCalculator { + func sum(left: Int, right: Int) -> SignalProducer +} + +fileprivate class TestReactiveCalculator: ReactiveCalculator { + init() {} + + let sum = ReactiveEventsCall<(left: Int, right: Int), Int, TestError>() + @discardableResult func sum(left: Int, right: Int) -> SignalProducer { + return stubCall(sum, argument: (left: left, right: right)) + } +} + +fileprivate struct TestError: Error, Equatable { + let id: Int + init() { id = 0 } + init(id: Int) { self.id = id } +} + +class SwiftyMockReactiveEventsCallsSpec: QuickSpec { + override func spec() { + describe("SwiftyMockReactiveEventsCalls") { + describe("when correctly setup") { + var sut: TestReactiveCalculator! + beforeEach { + sut = TestReactiveCalculator() + } + + context("before calling stubbed method") { + it("should tell that method wasn't called") { + expect(sut.sum.called).to(beFalsy()) + } + it("should have calls count equal to zero") { + expect(sut.sum.callsCount).to(equal(0)) + } + it("should not have captured argument") { + expect(sut.sum.capturedArgument).to(beNil()) + } + it("should not have captured arguments") { + expect(sut.sum.capturedArguments).to(beEmpty()) + } + } + + context("when calling method before stubbing") { + it("should return empty signal without any value") { + expect { sut.sum(left: 1,right: 2) }.to(complete()) + } + } + + context("when calling stubbed method") { + context("with value stub") { + beforeEach { + sut.sum.returns(.value(12)) + } + + it("should return stubbed value and complete") { + let result = sut.sum(left: 1, right: 2) + expect(result).to(sendEvents([.value(12), .completed])) + } + + it("should have calls count equal number of calls") { + sut.sum(left: 1, right: 2) + expect(sut.sum.callsCount).to(equal(1)) + + sut.sum(left: 2, right: 3) + sut.sum(left: 3, right: 5) + expect(sut.sum.callsCount).to(equal(3)) + } + + it("should tell that method was called") { + sut.sum(left: 1,right: 2) + expect(sut.sum.called).to(beTruthy()) + } + } + + context("with failure value stub") { + beforeEach { + sut.sum.returns(.failed(TestError())) + } + + it("should return stubbed error") { + expect(sut.sum(left: 1, right: 2)).to(fail(with: TestError())) + } + } + + context("with logic stub") { + beforeEach { + sut.sum.performs { .value($0.left - $0.right) } + } + + it("should calculate method based on the stubbed block") { + expect(sut.sum(left: 1, right: 2)).to(sendValue(-1)) + expect(sut.sum(left: 3, right: 2)).to(sendValue(1)) + } + + it("should have calls count equal number of calls") { + sut.sum(left: 1, right: 2) + expect(sut.sum.callsCount).to(equal(1)) + + sut.sum(left: 2, right: 3) + sut.sum(left: 3, right: 5) + expect(sut.sum.callsCount).to(equal(3)) + } + + it("tell that method was called") { + sut.sum(left: 1, right:2) + expect(sut.sum.called).to(beTruthy()) + } + } + + context("with failure logic stub") { + beforeEach { + sut.sum.performs { _ in [.failed(TestError())] } + } + + it("should return stubbed error") { + expect(sut.sum(left: 1, right: 2)).to(fail(with: TestError())) + } + } + + context("with value and logic stub") { + beforeEach { + sut.sum.returns(.value(12)) + sut.sum.performs { [.value($0.left + $0.right)] } + } + + it("should use logic stub instead of value") { + expect(sut.sum(left: 15, right: 12)).to(sendValue(27)) + } + } + + context("with value and failure logic stub") { + beforeEach { + sut.sum.returns(.value(12)) + sut.sum.performs { _ in [.failed(TestError())] } + } + + it("should use failure logic stub instead of value") { + expect(sut.sum(left: 15, right: 12)).to(fail(with: TestError())) + } + } + + context("with failure value and logic stub") { + beforeEach { + sut.sum.returns([.failed(TestError())]) + sut.sum.performs { .value($0.left + $0.right) } + } + + it("should use logic stub instead of failure value") { + expect(sut.sum(left: 15, right: 12)).to(sendValue(27)) + } + } + + context("with failure value and failure logic stub") { + beforeEach { + sut.sum.returns([.failed(TestError(id: 0))]) + sut.sum.performs { _ in [.failed(TestError(id: 1))] } + } + + it("should use failure logic stub instead of failure value") { + expect(sut.sum(left: 15, right: 12)).to(fail(with: TestError(id: 1))) + } + } + } + + context("when calling filtered value stubbed method") { + beforeEach { + sut.sum.returns([.value(10)]) + sut.sum.on { $0.left == 12 }.returns(.value(0)) + sut.sum.on { $0.right == 15 }.returns([.value(7)]) + sut.sum.on { $0.right == 42 }.returns([.failed(TestError())]) + } + context("when parameters matching filter") { + it("should return filter srubbed value") { + expect(sut.sum(left: 12, right: 2)).to(sendValue(0)) + expect(sut.sum(left: 0, right: 15)).to(sendValue(7)) + expect(sut.sum(left: 23, right: 42)).to(fail(with: TestError())) + } + } + context("when parameters don't match filters") { + it("should return default stubbed value") { + expect(sut.sum(left: 13, right: 2)).to(sendValue(10)) + } + } + } + + context("when calling filtered block stubbed method") { + beforeEach { + sut.sum.performs { [.value($0.left - $0.right)] } + sut.sum.on { $0.left == 0 }.performs { _ in [.value(0)] } + sut.sum.on { $0.right == 0 }.performs { _ in .value(12) } + sut.sum.on { $0.right == -1 }.performs { _ in [.failed(TestError())] } + } + context("when parameters matching filter") { + it("should return call filter-based block") { + expect(sut.sum(left: 0, right: 2)).to(sendValue(0)) + expect(sut.sum(left: 15, right: 0)).to(sendValue(12)) + expect(sut.sum(left: 15, right: -1)).to(fail(with: TestError())) + } + } + context("when parameters don't match filters") { + it("should call default stubbed block") { + expect(sut.sum(left: 13, right: 2)).to(sendValue(11)) + } + } + } + + context("when calling filtered stubbed with block method") { + beforeEach { + sut.sum.returns([.value(17)]) + sut.sum.on { $0.left == 12 }.performs { [.value($0.left - $0.right)] } + sut.sum.on { $0.right == 13 }.performs { _ in [.value(42), .interrupted] } + sut.sum.on { $0.left == 42 }.performs { _ in .failed(TestError()) } + } + context("when parameters matching filter") { + it("should return calculated with stub value") { + expect(sut.sum(left: 12, right: 2)).to(sendValue(10)) + expect(sut.sum(left: 12, right: 12)).to(sendValue(0)) + expect(sut.sum(left: 0, right: 13)).to(interrupt()) + expect(sut.sum(left: 1, right: 13)).to(sendEvents([.value(42), .interrupted])) + expect(sut.sum(left: 42, right: 12)).to(fail(with: TestError())) + } + } + context("when parameters don't match filters") { + it("should return default stubbed value") { + expect(sut.sum(left: 13, right: 2)).to(sendValue(17)) + } + } + } + } + } + } +} From aee2bb832229a419986b5f23a46f847fca3155ee Mon Sep 17 00:00:00 2001 From: a-voronov Date: Tue, 4 Sep 2018 21:49:47 +0300 Subject: [PATCH 6/7] added reactive events call description --- .../Mocks/Generated/Mock.generated.swift | 2 +- README.md | 20 ++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/Example/RoboKittenTests/Mocks/Generated/Mock.generated.swift b/Example/RoboKittenTests/Mocks/Generated/Mock.generated.swift index 9fe0dc2..fc8868d 100644 --- a/Example/RoboKittenTests/Mocks/Generated/Mock.generated.swift +++ b/Example/RoboKittenTests/Mocks/Generated/Mock.generated.swift @@ -1,4 +1,4 @@ -// Generated using Sourcery 0.13.1 — https://github.com/krzysztofzablocki/Sourcery +// Generated using Sourcery 0.14.0 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT import Foundation diff --git a/README.md b/README.md index c321d86..c4f46a2 100644 --- a/README.md +++ b/README.md @@ -181,8 +181,26 @@ kittenMock.batteryStatusCall.returns(Result(error: ImagineThisIsError)) Everything else stays the same :) +### Advanced +Usually Result is enough to stub signal with either value or error. But since Signal represents sequence of values over time, you might want to stub this behaviour as well. +For this purpose, there's `ReactiveEventsCall` which is actually a `FunctionCall` with `Array` value type. +This way you can stub function return value with sequence of events. But don't forget about [Event contract](https://github.com/ReactiveCocoa/ReactiveSwift/blob/master/Documentation/APIContracts.md#the-event-contract) - thus no value will be sent after any terminating event. + +```swift +// when using Result: +kittenMock.batteryStatusCall.returns(.success(42)) + +// when using Signal.Event - you can pass either one event, or many as shown below: +// also `.completed` event will be called automatically if no terminating event passed in the sequence, thus `[.value(42), .completed] == .value(42)` +kittenMock.batteryStatusCall.returns(.value(42)) +// or +kittenMock.batteryStatusCall.returns([.value(42), .value(12), .value(0), .completed]) +// but this one won't send `.value(0)`, since it has already completed before it +kittenMock.batteryStatusCall.returns([.value(42), .completed, .value(0)]) +``` + # Matchers -SwiftyMock doesn't have its own matchers, so you can use whatever matchers suits better for you :) +SwiftyMock doesn't have its own matchers, so you can use whatever matchers suit you better :) # Templates You can generate mocks automatically with [Sourcery](https://github.com/krzysztofzablocki/Sourcery). From 1eac460b9b4c7703fbc7e46158c1782a04df61d6 Mon Sep 17 00:00:00 2001 From: Alexander Voronov Date: Wed, 5 Sep 2018 11:00:10 +0300 Subject: [PATCH 7/7] added test to verify multiple events behaviour --- .../SwiftyMockReactiveEventsCallsSpec.swift | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/Example/Tests/Calls/SwiftyMockReactiveEventsCallsSpec.swift b/Example/Tests/Calls/SwiftyMockReactiveEventsCallsSpec.swift index d27e779..c6600be 100644 --- a/Example/Tests/Calls/SwiftyMockReactiveEventsCallsSpec.swift +++ b/Example/Tests/Calls/SwiftyMockReactiveEventsCallsSpec.swift @@ -88,6 +88,109 @@ class SwiftyMockReactiveEventsCallsSpec: QuickSpec { } } + context("with multiple values stub") { + beforeEach { + sut.sum.returns([.value(1), .value(1), .value(2), .value(6), .value(24), .value(120), .value(720), .completed]) + } + + it("should return stubbed values and complete") { + let result = sut.sum(left: 1, right: 2) + expect(result).to(sendEvents([.value(1), .value(1), .value(2), .value(6), .value(24), .value(120), .value(720), .completed])) + } + } + + context("with multiple values without terminating event stub") { + beforeEach { + sut.sum.returns([.value(1), .value(1), .value(2)]) + } + + it("should return stubbed values and complete") { + let result = sut.sum(left: 1, right: 2) + expect(result).to(sendEvents([.value(1), .value(1), .value(2), .completed])) + } + } + + context("with terminating event in the middle of values stub") { + context("in case of completed event") { + beforeEach { + sut.sum.returns([.value(1), .value(1), .completed, .value(2)]) + } + + it("should return stubbed values only before completed event") { + let result = sut.sum(left: 1, right: 2) + expect(result).to(sendEvents([.value(1), .value(1), .completed])) + } + } + + context("in case of interrupted event") { + beforeEach { + sut.sum.returns([.value(1), .value(1), .interrupted, .value(2)]) + } + + it("should return stubbed values only before completed event") { + let result = sut.sum(left: 1, right: 2) + expect(result).to(sendEvents([.value(1), .value(1), .interrupted])) + } + } + + context("in case of failed event") { + beforeEach { + sut.sum.returns([.value(1), .value(1), .failed(TestError()), .value(2)]) + } + + it("should return stubbed values only before completed event") { + let result = sut.sum(left: 1, right: 2) + expect(result).to(sendEvents([.value(1), .value(1), .failed(TestError())])) + } + } + } + + context("with terminating event in the end of values stub") { + context("in case of completed event") { + beforeEach { + sut.sum.returns([.value(1), .value(1), .value(2), .completed]) + } + + it("should return whole sequence of events") { + let result = sut.sum(left: 1, right: 2) + expect(result).to(sendEvents([.value(1), .value(1), .value(2), .completed])) + } + } + + context("in case of interrupted event") { + beforeEach { + sut.sum.returns([.value(1), .value(1), .value(2), .interrupted]) + } + + it("should return whole sequence of events") { + let result = sut.sum(left: 1, right: 2) + expect(result).to(sendEvents([.value(1), .value(1), .value(2), .interrupted])) + } + } + + context("in case of failed event") { + beforeEach { + sut.sum.returns([.value(1), .value(1), .value(2), .failed(TestError())]) + } + + it("should return whole sequence of events") { + let result = sut.sum(left: 1, right: 2) + expect(result).to(sendEvents([.value(1), .value(1), .value(2), .failed(TestError())])) + } + } + + context("in case of multiple terminating events") { + beforeEach { + sut.sum.returns([.value(1), .value(1), .value(2), .interrupted, .failed(TestError()), .completed]) + } + + it("should return only events up until first terminating event, including it") { + let result = sut.sum(left: 1, right: 2) + expect(result).to(sendEvents([.value(1), .value(1), .value(2), .interrupted])) + } + } + } + context("with failure value stub") { beforeEach { sut.sum.returns(.failed(TestError()))