diff --git a/Cuckoo.xcodeproj/project.pbxproj b/Cuckoo.xcodeproj/project.pbxproj index d6fa6f5..96b2bbf 100644 --- a/Cuckoo.xcodeproj/project.pbxproj +++ b/Cuckoo.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 183D03FF1C4691C600EBAEF3 /* Cuckoo.h in Headers */ = {isa = PBXBuildFile; fileRef = 183D03FE1C4691C600EBAEF3 /* Cuckoo.h */; settings = {ATTRIBUTES = (Public, ); }; }; 183D04061C4691C600EBAEF3 /* Cuckoo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 183D03FB1C4691C600EBAEF3 /* Cuckoo.framework */; }; 183D04161C46926A00EBAEF3 /* CuckooFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 183D04151C46926A00EBAEF3 /* CuckooFunctions.swift */; }; + DC1A82B51D2D6BD500A217F0 /* FailTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1A82B31D2D6A2100A217F0 /* FailTest.swift */; }; DC4094EE1D211563006FB137 /* StubFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4094EC1D211563006FB137 /* StubFunction.swift */; }; DC4094EF1D211563006FB137 /* StubThrowingFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4094ED1D211563006FB137 /* StubThrowingFunction.swift */; }; DC4094F31D211598006FB137 /* ToBeStubbedProperty.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4094F11D211598006FB137 /* ToBeStubbedProperty.swift */; }; @@ -82,6 +83,7 @@ 183D04051C4691C600EBAEF3 /* CuckooTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CuckooTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 183D040C1C4691C600EBAEF3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 183D04151C46926A00EBAEF3 /* CuckooFunctions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CuckooFunctions.swift; sourceTree = ""; }; + DC1A82B31D2D6A2100A217F0 /* FailTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FailTest.swift; path = ../../../Brightify/Cuckoo/Tests/FailTest.swift; sourceTree = ""; }; DC4094EC1D211563006FB137 /* StubFunction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = StubFunction.swift; path = Stubbing/StubFunction/StubFunction.swift; sourceTree = ""; }; DC4094ED1D211563006FB137 /* StubThrowingFunction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = StubThrowingFunction.swift; path = Stubbing/StubFunction/StubThrowingFunction.swift; sourceTree = ""; }; DC4094F11D211598006FB137 /* ToBeStubbedProperty.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ToBeStubbedProperty.swift; path = Stubbing/ToBeStubbedProperty/ToBeStubbedProperty.swift; sourceTree = ""; }; @@ -198,6 +200,7 @@ 183D040C1C4691C600EBAEF3 /* Info.plist */, DC70D0A91D2AB5F500014C5F /* ClassTest.swift */, DC70D0A61D2AB5B300014C5F /* CuckooFunctionsTest.swift */, + DC1A82B31D2D6A2100A217F0 /* FailTest.swift */, DC70D0AC1D2AB62100014C5F /* ProtocolTest.swift */, DC70D0B71D2ABA3100014C5F /* TestUtils.swift */, DC70D0A01D2AB53D00014C5F /* Describing */, @@ -547,6 +550,7 @@ DC70D0B31D2AB6B900014C5F /* MatchableTest.swift in Sources */, DC70D0BE1D2AF40800014C5F /* TestedClass.swift in Sources */, DC70D0D21D2B007300014C5F /* GeneratedMocks.swift in Sources */, + DC1A82B51D2D6BD500A217F0 /* FailTest.swift in Sources */, DC70D0D41D2B026200014C5F /* VerificationTest.swift in Sources */, DC70D0AD1D2AB62100014C5F /* ProtocolTest.swift in Sources */, DC70D0C51D2AFA8E00014C5F /* StubFunctionTest.swift in Sources */, diff --git a/Source/CuckooFunctions.swift b/Source/CuckooFunctions.swift index fc4a81c..b70cc27 100644 --- a/Source/CuckooFunctions.swift +++ b/Source/CuckooFunctions.swift @@ -19,8 +19,8 @@ public func when(function: F) -> F { /// Creates object used for verification of calls. @warn_unused_result -public func verify(mock: M, _ callMatcher: CallMatcher = times(1), sourceLocation: SourceLocation = (#file, #line)) -> M.Verification { - return mock.getVerificationProxy(callMatcher, sourceLocation: sourceLocation) +public func verify(mock: M, _ callMatcher: CallMatcher = times(1), file: StaticString = #file, line: UInt = #line) -> M.Verification { + return mock.getVerificationProxy(callMatcher, sourceLocation: (file, line)) } /// Clears all invocations and stubs of mocks. @@ -45,8 +45,8 @@ public func clearInvocations(mocks: M...) { } /// Checks if there are no more uverified calls. -public func verifyNoMoreInteractions(mocks: M..., sourceLocation: SourceLocation = (#file, #line)) { +public func verifyNoMoreInteractions(mocks: M..., file: StaticString = #file, line: UInt = #line) { mocks.forEach { mock in - mock.manager.verifyNoMoreInteractions(sourceLocation) + mock.manager.verifyNoMoreInteractions((file, line)) } } \ No newline at end of file diff --git a/Source/Matching/CallMatcher.swift b/Source/Matching/CallMatcher.swift index cc55169..3032dc1 100644 --- a/Source/Matching/CallMatcher.swift +++ b/Source/Matching/CallMatcher.swift @@ -8,34 +8,35 @@ /// CallMatcher is used in verification to assert how many times was the call made. It can also be used to do different asserts on stub calls matched with parameter matchers. public struct CallMatcher { - private let matchesFunction: [StubCall] throws -> Bool + public let name: String - public init(matchesFunction: [StubCall] -> Bool = { _ in true }) { + private let matchesFunction: [StubCall] -> Bool + + public init(name: String, matchesFunction: [StubCall] -> Bool) { + self.name = name self.matchesFunction = matchesFunction } - public init(numberOfExpectedCalls: Int, compareCallsFunction: (expected: Int, actual: Int) -> Bool) { - self.matchesFunction = { + public init(name: String, numberOfExpectedCalls: Int, compareCallsFunction: (expected: Int, actual: Int) -> Bool) { + self.init(name: name) { return compareCallsFunction(expected: numberOfExpectedCalls, actual: $0.count) } } public func matches(calls: [StubCall]) -> Bool { - do { - return try matchesFunction(calls) - } catch { - return false - } + return matchesFunction(calls) } public func or(otherMatcher: CallMatcher) -> CallMatcher { - return CallMatcher { + let name = "either \(self.name) or \(otherMatcher.name)" + return CallMatcher(name: name) { return self.matches($0) || otherMatcher.matches($0) } } public func and(otherMatcher: CallMatcher) -> CallMatcher { - return CallMatcher { + let name = "both \(self.name) and \(otherMatcher.name)" + return CallMatcher(name: name) { return self.matches($0) && otherMatcher.matches($0) } } diff --git a/Source/Matching/CallMatcherFunctions.swift b/Source/Matching/CallMatcherFunctions.swift index ad5d870..4b7c183 100644 --- a/Source/Matching/CallMatcherFunctions.swift +++ b/Source/Matching/CallMatcherFunctions.swift @@ -9,7 +9,8 @@ /// Returns a matcher ensuring a call was made **`count`** times. @warn_unused_result public func times(count: Int) -> CallMatcher { - return CallMatcher(numberOfExpectedCalls: count, compareCallsFunction: ==) + let name = count == 0 ? "never" : "\(count) times" + return CallMatcher(name: name, numberOfExpectedCalls: count, compareCallsFunction: ==) } /// Returns a matcher ensuring no call was made. @@ -27,11 +28,11 @@ public func atLeastOnce() -> CallMatcher { /// Returns a matcher ensuring call was made at least `count` times. @warn_unused_result public func atLeast(count: Int) -> CallMatcher { - return CallMatcher(numberOfExpectedCalls: count, compareCallsFunction: <=) + return CallMatcher(name: "at least \(count) times", numberOfExpectedCalls: count, compareCallsFunction: <=) } /// Returns a matcher ensuring call was made at most `count` times. @warn_unused_result public func atMost(count: Int) -> CallMatcher { - return CallMatcher(numberOfExpectedCalls: count, compareCallsFunction: >=) + return CallMatcher(name: "at most \(count) times",numberOfExpectedCalls: count, compareCallsFunction: >=) } diff --git a/Source/Matching/ParameterMatcher.swift b/Source/Matching/ParameterMatcher.swift index 8678767..cdadd82 100644 --- a/Source/Matching/ParameterMatcher.swift +++ b/Source/Matching/ParameterMatcher.swift @@ -8,7 +8,7 @@ /// ParameterMatcher matches parameters of methods in stubbing and verification. public struct ParameterMatcher: Matchable { - private let matchesFunction: T throws -> Bool + private let matchesFunction: T -> Bool public init(matchesFunction: T -> Bool = { _ in true }) { self.matchesFunction = matchesFunction @@ -19,10 +19,6 @@ public struct ParameterMatcher: Matchable { } public func matches(input: T) -> Bool { - do { - return try matchesFunction(input) - } catch { - return false - } + return matchesFunction(input) } } \ No newline at end of file diff --git a/Source/MockManager.swift b/Source/MockManager.swift index 7fabee5..bf426ca 100644 --- a/Source/MockManager.swift +++ b/Source/MockManager.swift @@ -84,8 +84,8 @@ public class MockManager { unverifiedStubCallsIndexes = unverifiedStubCallsIndexes.filter { !indexesToRemove.contains($0) } if callMatcher.matches(calls) == false { - let description = Description() - MockManager.fail(message: description.description, sourceLocation: sourceLocation) + let message = "Wanted \(callMatcher.name) but \(calls.count == 0 ? "not invoked" : "invoked \(calls.count) times")." + MockManager.fail(message: message, sourceLocation: sourceLocation) } return __DoNotUse() } @@ -106,14 +106,26 @@ public class MockManager { func verifyNoMoreInteractions(sourceLocation: SourceLocation) { if unverifiedStubCallsIndexes.isEmpty == false { - let unverifiedCalls = unverifiedStubCallsIndexes.map { stubCalls[$0] }.map { String($0) }.joinWithSeparator(", ") - MockManager.fail(message: "Found unverified call(s): " + unverifiedCalls, sourceLocation: sourceLocation) + let unverifiedCalls = unverifiedStubCallsIndexes.map { stubCalls[$0] }.map { call in + if let bracketIndex = call.method.rangeOfString("(")?.startIndex { + let name = call.method.substringToIndex(bracketIndex) + return name + call.parametersAsString + } else { + if call.method.hasSuffix("#set") { + return call.method + call.parametersAsString + } else { + return call.method + } + } + }.enumerate().map { "\($0 + 1). " + $1 }.joinWithSeparator("\n") + let message = "No more interactions wanted but some found:\n" + MockManager.fail(message: message + unverifiedCalls, sourceLocation: sourceLocation) } } @noreturn - private func failAndCrash(message: String, sourceLocation: SourceLocation = (#file, #line)) { - MockManager.fail(message: message, sourceLocation: sourceLocation) + private func failAndCrash(message: String, file: StaticString = #file, line: UInt = #line) { + MockManager.fail(message: message, sourceLocation: (file, line)) fatalError(message) } } diff --git a/Source/stubbing/StubCall.swift b/Source/stubbing/StubCall.swift index 5055f19..92b25b8 100644 --- a/Source/stubbing/StubCall.swift +++ b/Source/stubbing/StubCall.swift @@ -8,11 +8,23 @@ public protocol StubCall { var method: String { get } + var parametersAsString: String { get } } public struct ConcreteStubCall: StubCall { public let method: String - let parameters: IN + public let parameters: IN + + public var parametersAsString: String { + let string = String(parameters) + if (string.rangeOfString(",") != nil && string.hasPrefix("(")) || string == "()" { + return string + } else { + // If only one parameter add brackets and quotes + let wrappedParameter = String((parameters, 0)) + return wrappedParameter.substringToIndex(wrappedParameter.endIndex.advancedBy(-4)) + ")" + } + } public init(method: String, parameters: IN) { self.method = method diff --git a/Tests/CuckooFunctionsTest.swift b/Tests/CuckooFunctionsTest.swift index 427faa4..f090ada 100644 --- a/Tests/CuckooFunctionsTest.swift +++ b/Tests/CuckooFunctionsTest.swift @@ -64,19 +64,4 @@ class CuckooFunctionsTest: XCTestCase { verifyNoMoreInteractions(mock) } - - func testVerifyNoMoreInteractionsFail() { - let error = TestUtils.catchCuckooFail { - let mock = MockTestedClass() - stub(mock) { mock in - when(mock.noReturn()).thenDoNothing() - } - - mock.noReturn() - - verifyNoMoreInteractions(mock) - } - - XCTAssertNotNil(error) - } } diff --git a/Tests/FailTest.swift b/Tests/FailTest.swift new file mode 100644 index 0000000..8122d70 --- /dev/null +++ b/Tests/FailTest.swift @@ -0,0 +1,133 @@ +// +// FailTest.swift +// Cuckoo +// +// Created by Filip Dolnik on 06.07.16. +// Copyright © 2016 Brightify. All rights reserved. +// + +import XCTest +import Cuckoo + +class FailTest: XCTestCase { + + func testMissingInvocation() { + let error = TestUtils.catchCuckooFail { + let mock = MockTestedClass() + + verify(mock).noReturn() + } + + XCTAssertEqual(error, "Wanted 1 times but not invoked.") + } + + func testNoInvocation2Wanted() { + let error = TestUtils.catchCuckooFail { + let mock = MockTestedClass() + stub(mock) { mock in + when(mock.noReturn()).thenDoNothing() + } + + mock.noReturn() + + verify(mock, times(2)).noReturn() + } + + XCTAssertEqual(error, "Wanted 2 times but invoked 1 times.") + } + + func testInvocationNeverWanted() { + let error = TestUtils.catchCuckooFail { + let mock = MockTestedClass() + stub(mock) { mock in + when(mock.noReturn()).thenDoNothing() + } + + mock.noReturn() + + verify(mock, never()).noReturn() + } + + XCTAssertEqual(error, "Wanted never but invoked 1 times.") + } + + func testInvocationAtLeast2Wanted() { + let error = TestUtils.catchCuckooFail { + let mock = MockTestedClass() + stub(mock) { mock in + when(mock.noReturn()).thenDoNothing() + } + + mock.noReturn() + + verify(mock, atLeast(2)).noReturn() + } + + XCTAssertEqual(error, "Wanted at least 2 times but invoked 1 times.") + } + + func test2InvocationAtMost1Wanted() { + let error = TestUtils.catchCuckooFail { + let mock = MockTestedClass() + stub(mock) { mock in + when(mock.noReturn()).thenDoNothing() + } + + mock.noReturn() + mock.noReturn() + + verify(mock, atMost(1)).noReturn() + } + + XCTAssertEqual(error, "Wanted at most 1 times but invoked 2 times.") + } + + func testCallMatcherOr() { + let error = TestUtils.catchCuckooFail { + let mock = MockTestedClass() + stub(mock) { mock in + when(mock.noReturn()).thenDoNothing() + } + + verify(mock, times(1).or(times(2))).noReturn() + } + + XCTAssertEqual(error, "Wanted either 1 times or 2 times but not invoked.") + } + + func testCallMatcherAnd() { + let error = TestUtils.catchCuckooFail { + let mock = MockTestedClass() + stub(mock) { mock in + when(mock.noReturn()).thenDoNothing() + } + + verify(mock, atLeast(1).and(atMost(2))).noReturn() + } + + XCTAssertEqual(error, "Wanted both at least 1 times and at most 2 times but not invoked.") + } + + func testVerifyNoMoreInteractionsFail() { + let error = TestUtils.catchCuckooFail { + let mock = MockTestedClass(spyOn: TestedClass()) + + mock.withOptionalClosure("a", closure: nil) + mock.noReturn() + mock.countCharacters("b") + let _ = mock.readWriteProperty + mock.readWriteProperty = 1 + mock.withOptionalClosure("c", closure: { _ in }) + + verifyNoMoreInteractions(mock) + } + + XCTAssertEqual(error, "No more interactions wanted but some found:\n" + + "1. withOptionalClosure(\"a\", nil)\n" + + "2. noReturn()\n" + + "3. countCharacters(\"b\")\n" + + "4. readWriteProperty#get\n" + + "5. readWriteProperty#set(1)\n" + + "6. withOptionalClosure(\"c\", Optional((Function)))") + } +} diff --git a/Tests/Matching/CallMatcherTest.swift b/Tests/Matching/CallMatcherTest.swift index 564ba91..f6f0a87 100644 --- a/Tests/Matching/CallMatcherTest.swift +++ b/Tests/Matching/CallMatcherTest.swift @@ -17,7 +17,7 @@ class CallMatcherTest: XCTestCase { } func testMatches() { - let matcher = CallMatcher { ($0.first?.method ?? "") == "A"} + let matcher = CallMatcher(name: "") { ($0.first?.method ?? "") == "A"} let nonMatchingCalls = [ConcreteStubCall(method: "B", parameters: Void()) as StubCall] XCTAssertTrue(matcher.matches(call)) diff --git a/Tests/Verification/VerificationTest.swift b/Tests/Verification/VerificationTest.swift index a87f16b..734cfbd 100644 --- a/Tests/Verification/VerificationTest.swift +++ b/Tests/Verification/VerificationTest.swift @@ -47,14 +47,4 @@ class VerificationTest: XCTestCase { verify(mock).noReturn() verify(mock).countCharacters(anyString()) } - - func testVerifyFail() { - let error = TestUtils.catchCuckooFail { - let mock = MockTestedClass() - - verify(mock).noReturn() - } - - XCTAssertNotNil(error) - } } \ No newline at end of file