diff --git a/Source/CuckooFunctions.swift b/Source/CuckooFunctions.swift index b3dee11e..b6eab754 100644 --- a/Source/CuckooFunctions.swift +++ b/Source/CuckooFunctions.swift @@ -9,8 +9,8 @@ public func when(_ function: F) -> F { } /// Creates object used for verification of calls. -public func verify(_ mock: M, _ callMatcher: CallMatcher = times(1), file: StaticString = #file, line: UInt = #line) -> M.Verification { - return mock.getVerificationProxy(callMatcher, sourceLocation: (file, line)) +public func verify(_ mock: M, _ callMatcher: CallMatcher = times(1), file: StaticString = #file, fileID: String = #fileID, filePath: String = #filePath, line: Int = #line, column: Int = #column) -> M.Verification { + return mock.getVerificationProxy(callMatcher, sourceLocation: (file, fileID, filePath, line, column)) } /// Clears all invocations and stubs of mocks. @@ -35,8 +35,8 @@ public func clearInvocations(_ mocks: M...) { } /// Checks if there are no more uverified calls. -public func verifyNoMoreInteractions(_ mocks: M..., file: StaticString = #file, line: UInt = #line) { +public func verifyNoMoreInteractions(_ mocks: M..., file: StaticString = #file, fileID: String = #fileID, filePath: String = #filePath, line: Int = #line, column: Int = #column) { mocks.forEach { mock in - mock.cuckoo_manager.verifyNoMoreInteractions((file, line)) + mock.cuckoo_manager.verifyNoMoreInteractions((file, fileID, filePath, line, column)) } } diff --git a/Source/MockManager.swift b/Source/MockManager.swift index c8b60e50..a8f02e77 100644 --- a/Source/MockManager.swift +++ b/Source/MockManager.swift @@ -1,3 +1,6 @@ +#if canImport(Testing) +import Testing +#endif #if canImport(XCTest) import XCTest #endif @@ -6,8 +9,20 @@ public class MockManager { public static var fail: ((message: String, sourceLocation: SourceLocation)) -> Void = { arg in let (message, sourceLocation) = arg #if canImport(XCTest) - XCTFail(message, file: sourceLocation.file, line: sourceLocation.line) - #else + XCTFail(message, file: sourceLocation.file, line: UInt(sourceLocation.line)) + #endif + #if canImport(Testing) + Issue.record( + Comment(rawValue: message), + sourceLocation: Testing.SourceLocation( + fileID: sourceLocation.fileID, + filePath: sourceLocation.filePath, + line: sourceLocation.line, + column: sourceLocation.column + ) + ) + #endif + #if !canImport(XCTest) && !canImport(Testing) print("\(Self.self).fail:", message, sourceLocation) #endif } @@ -277,8 +292,8 @@ public class MockManager { } } - private func failAndCrash(_ message: String, file: StaticString = #file, line: UInt = #line) -> Never { - MockManager.fail((message: message, sourceLocation: (file, line))) + private func failAndCrash(_ message: String, file: StaticString = #file, fileID: String = #fileID, filePath: String = #filePath, line: Int = #line, column: Int = #column) -> Never { + MockManager.fail((message: message, sourceLocation: (file, fileID, filePath, line, column))) #if _runtime(_ObjC) NSException(name: .internalInconsistencyException, reason: message, userInfo: nil).raise() diff --git a/Source/Utils.swift b/Source/Utils.swift index a4e76b16..56bd4200 100644 --- a/Source/Utils.swift +++ b/Source/Utils.swift @@ -18,4 +18,4 @@ public func wrap(matchable: M, mapping: @escaping ( } } -public typealias SourceLocation = (file: StaticString, line: UInt) +public typealias SourceLocation = (file: StaticString, fileID: String, filePath: String, line: Int, column: Int) diff --git a/Tests/Swift/FailTest.swift b/Tests/Swift/FailTest.swift index d6a07a77..d21c4564 100644 --- a/Tests/Swift/FailTest.swift +++ b/Tests/Swift/FailTest.swift @@ -1,3 +1,4 @@ +import Testing import XCTest @testable import Cuckoo @testable import CuckooMocks @@ -123,3 +124,150 @@ final class FailTest: XCTestCase { "6. withOptionalClosure(\"c\", Optional((Function)))"].joined(separator: "\n")) } } + +struct FailSwiftTestingTests { + @Test + func missingInvocation() { + withKnownIssue { + let mock = MockTestedClass() + + verify(mock).noReturn() + } matching: { issue in + issue.comments.map(\.rawValue) == [ + "Wanted 1 times but not invoked." + ] + } + } + + @Test + func noInvocation2Wanted() { + withKnownIssue { + let mock = MockTestedClass() + stub(mock) { mock in + when(mock.noReturn()).thenDoNothing() + } + + mock.noReturn() + + verify(mock, times(2)).noReturn() + } matching: { issue in + issue.comments.map(\.rawValue) == [ + "Wanted 2 times but invoked 1 times." + ] + } + } + + @Test + func invocationNeverWanted() { + withKnownIssue { + let mock = MockTestedClass() + stub(mock) { mock in + when(mock.noReturn()).thenDoNothing() + } + + mock.noReturn() + + verify(mock, never()).noReturn() + } matching: { issue in + issue.comments.map(\.rawValue) == [ + "Wanted never but invoked 1 times." + ] + } + } + + @Test + func invocationAtLeast2Wanted() { + withKnownIssue { + let mock = MockTestedClass() + stub(mock) { mock in + when(mock.noReturn()).thenDoNothing() + } + + mock.noReturn() + + verify(mock, atLeast(2)).noReturn() + } matching: { issue in + issue.comments.map(\.rawValue) == [ + "Wanted at least 2 times but invoked 1 times." + ] + } + } + + @Test + func invocationAtMost1Wanted() { + withKnownIssue { + let mock = MockTestedClass() + stub(mock) { mock in + when(mock.noReturn()).thenDoNothing() + } + + mock.noReturn() + mock.noReturn() + + verify(mock, atMost(1)).noReturn() + } matching: { issue in + issue.comments.map(\.rawValue) == [ + "Wanted at most 1 times but invoked 2 times." + ] + } + } + + @Test + func callMatcherOr() { + withKnownIssue { + let mock = MockTestedClass() + stub(mock) { mock in + when(mock.noReturn()).thenDoNothing() + } + + verify(mock, times(1).or(times(2))).noReturn() + } matching: { issue in + issue.comments.map(\.rawValue) == [ + "Wanted either 1 times or 2 times but not invoked." + ] + } + } + + @Test + func callMatcherAnd() { + withKnownIssue { + let mock = MockTestedClass() + stub(mock) { mock in + when(mock.noReturn()).thenDoNothing() + } + + verify(mock, atLeast(1).and(atMost(2))).noReturn() + } matching: { issue in + issue.comments.map(\.rawValue) == [ + "Wanted both at least 1 times and at most 2 times but not invoked." + ] + } + } + + @Test + func verifyNoMoreInteractionsFail() { + withKnownIssue { + let mock = MockTestedClass().withEnabledSuperclassSpy() + + mock.withOptionalClosure("a", closure: nil) + mock.noReturn() + _ = mock.count(characters: "b") + let _ = mock.readWriteProperty + mock.readWriteProperty = 1 + mock.withOptionalClosure("c", closure: { _ in }) + + verifyNoMoreInteractions(mock) + } matching: { issue in + issue.comments.map(\.rawValue) == [ + ["No more interactions wanted but some found:", + "1. withOptionalClosure(\"a\", nil)", + "2. noReturn()", + "3. count(\"b\")", + "4. readWriteProperty#get", + "5. readWriteProperty#set(1)", + "6. withOptionalClosure(\"c\", Optional((Function)))" + ].joined(separator: "\n") + ] + } + } +} diff --git a/Tests/Swift/StubTest.swift b/Tests/Swift/StubTest.swift index 6c30a907..8299507a 100644 --- a/Tests/Swift/StubTest.swift +++ b/Tests/Swift/StubTest.swift @@ -1,5 +1,6 @@ -import XCTest import Cuckoo +import Testing +import XCTest @testable import CuckooMocks final class StubTest: XCTestCase { @@ -86,3 +87,97 @@ final class StubTest: XCTestCase { stub.voidFunction() } } + +struct StubSwiftTestingTests { + let stub = ClassForStubTestingStub() + + @Test + func intProperty() { + stub.intProperty = 1 + #expect(stub.intProperty == 0) + } + + @Test + func arrayProperty() { + stub.arrayProperty = [1] + #expect(stub.arrayProperty == []) + } + + @Test + func setProperty() { + stub.setProperty = Set([1]) + #expect(stub.setProperty == Set()) + } + + @Test + func dictionaryProperty() { + stub.dictionaryProperty = ["a": 1] + #expect(stub.dictionaryProperty == [:]) + } + + @Test + func tuple1() { + stub.tuple1 = (2) + #expect(stub.tuple1 == (0)) + } + + @Test + func tuple2() { + stub.tuple2 = (2, 1) + #expect(stub.tuple2 == (0, 0)) + } + + @Test + func tuple3() { + stub.tuple3 = (2, 1, true) + #expect(stub.tuple3 == (0, 0, false)) + } + + @Test + func tuple4() { + stub.tuple4 = (2, 1, 1, 1) + #expect(stub.tuple4 == (0, 0, 0, 0)) + } + + @Test + func tuple5() { + stub.tuple5 = (2, 1, 1, 1, 1) + #expect(stub.tuple5 == (0, 0, 0, 0, 0)) + } + + @Test + func tuple6() { + stub.tuple6 = (2, "A", 1, 1, 1, 1) + #expect(stub.tuple6 == (0, "", 0, 0, 0, 0)) + } + + @Test + func intFunction() { + stub.intProperty = 1 + #expect(stub.intFunction() == 0) + } + + @Test + func arrayFunction() { + stub.arrayProperty = [1] + #expect(stub.arrayFunction() == []) + } + + @Test + func setFunction() { + stub.setProperty = Set([1]) + #expect(stub.setFunction() == Set()) + } + + @Test + func dictionaryFunction() { + stub.dictionaryProperty = ["a": 1] + #expect(stub.dictionaryFunction() == [:]) + } + + @Test + func testVoidFunction() { + // Test for crash + stub.voidFunction() + } +}