diff --git a/README.md b/README.md index 2e9d661..27911f1 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,7 @@ assertCustom( tests: { pair, file, line in myCustomAssertion( pair.left, pair.right, + message: pair.message, file: file, line: line // <-- ⚠️ this is important! ) try youCanAlsoThrowErrorsInHere() // They will also get attributed to the correct line. diff --git a/Sources/TestCleaner/TestCleaner.swift b/Sources/TestCleaner/TestCleaner.swift index f24e651..9d7e4fe 100644 --- a/Sources/TestCleaner/TestCleaner.swift +++ b/Sources/TestCleaner/TestCleaner.swift @@ -22,6 +22,9 @@ public extension XCTestCase { /// The right-hand value in the pair. Might represent the expected value in a test, or the right-hand side of a comparison expression. let rightClosure: () -> Right + /// An optional description of the failure. + let messageClosure: () -> String + /// The file in which the `TestPair` was initialized. let file: StaticString @@ -31,31 +34,41 @@ public extension XCTestCase { /// The involvement of this `TestPair`. public let involvement: involvement? + /// The left-hand value in the pair. Might represent the observed value in a test, or the left-hand side of a comparison expression. public var left: Left { leftClosure() } + /// The right-hand value in the pair. Might represent the expected value in a test, or the right-hand side of a comparison expression. public var right: Right { rightClosure() } + /// An optional description of the failure. + public var message: String { + messageClosure() + } + /// Initializes a new `TestPair`, capturing the file and line information for use in `XCTest` methods. /// - Parameters: /// - left: a closure that generates the left-hand value. /// - right: the closure that generates the right-hand value. /// - involvement: the involvement of this test pair. + /// - message: an optional description of the failure. /// - file: the file in which the `TestPair` was initialized. /// - line: the line in which the `TestPair` was initialized. - init( - _ left: @escaping () -> Left, - _ right: @escaping () -> Right, - involvement: involvement?, - file: StaticString = #filePath, - line: UInt = #line - ) { + init( + _ left: @escaping () -> Left, + _ right: @escaping () -> Right, + involvement: involvement?, + message: @escaping () -> String, + file: StaticString = #filePath, + line: UInt = #line + ) { self.leftClosure = left self.rightClosure = right self.involvement = involvement + self.messageClosure = message self.file = file self.line = line } @@ -71,30 +84,33 @@ public extension XCTestCase { func Pair( _ left: @autoclosure @escaping () -> Left, _ right: @autoclosure @escaping () -> Right, + _ message: @autoclosure @escaping () -> String = "", file: StaticString = #filePath, line: UInt = #line ) -> TestPair { - TestPair(left, right, involvement: nil, file: file, line: line) + TestPair(left, right, involvement: nil, message: message, file: file, line: line) } /// Creates a test pair that is skipped when running the enclosing test. func xPair( _ left: @autoclosure @escaping () -> Left, _ right: @autoclosure @escaping () -> Right, + _ message: @autoclosure @escaping () -> String = "", file: StaticString = #filePath, line: UInt = #line ) -> TestPair { - TestPair(left, right, involvement: .excluded, file: file, line: line) + TestPair(left, right, involvement: .excluded, message: message, file: file, line: line) } /// Creates a test pair that is always run when the enclosing test is run. Causes any non-focused pairs to be skipped. If a test contains multiple focused pairs, they will all be run. func fPair( _ left: @autoclosure @escaping () -> Left, _ right: @autoclosure @escaping () -> Right, + _ message: @autoclosure @escaping () -> String = "", file: StaticString = #filePath, line: UInt = #line ) -> TestPair { - TestPair(left, right, involvement: .focused, file: file, line: line) + TestPair(left, right, involvement: .focused, message: message, file: file, line: line) } } @@ -106,9 +122,19 @@ public extension XCTestCase { func assertBoolean(testCases: [TestPair]) { for testCase in testCases.pairsToTest { if testCase.rightClosure() { - XCTAssertTrue(testCase.leftClosure(), file: testCase.file, line: testCase.line) + XCTAssertTrue( + testCase.leftClosure(), + testCase.message, + file: testCase.file, + line: testCase.line + ) } else { - XCTAssertFalse(testCase.leftClosure(), file: testCase.file, line: testCase.line) + XCTAssertFalse( + testCase.leftClosure(), + testCase.message, + file: testCase.file, + line: testCase.line + ) } } } @@ -117,7 +143,13 @@ public extension XCTestCase { /// - Parameter testCases: the cases to test, with the test value on the left and the value it is expected to be less than on the right. func assertLessThan(testCases: [TestPair]) { for testCase in testCases.pairsToTest { - XCTAssertLessThan(testCase.leftClosure(), testCase.rightClosure(), file: testCase.file, line: testCase.line) + XCTAssertLessThan( + testCase.leftClosure(), + testCase.rightClosure(), + testCase.message, + file: testCase.file, + line: testCase.line + ) } } @@ -125,7 +157,13 @@ public extension XCTestCase { /// - Parameter testCases: the cases to test, with the test value on the left and the value it is expected to be greater than on the right. func assertGreaterThan(testCases: [TestPair]) { for testCase in testCases.pairsToTest { - XCTAssertGreaterThan(testCase.leftClosure(), testCase.rightClosure(), file: testCase.file, line: testCase.line) + XCTAssertGreaterThan( + testCase.leftClosure(), + testCase.rightClosure(), + testCase.message, + file: testCase.file, + line: testCase.line + ) } } @@ -133,7 +171,13 @@ public extension XCTestCase { /// - Parameter testCases: the cases to test, with the test value on the left and the value it is expected to be less than or equal to on the right. func assertLessThanOrEqual(testCases: [TestPair]) { for testCase in testCases.pairsToTest { - XCTAssertLessThanOrEqual(testCase.leftClosure(), testCase.rightClosure(), file: testCase.file, line: testCase.line) + XCTAssertLessThanOrEqual( + testCase.leftClosure(), + testCase.rightClosure(), + testCase.message, + file: testCase.file, + line: testCase.line + ) } } @@ -141,7 +185,13 @@ public extension XCTestCase { /// - Parameter testCases: the cases to test, with the test value on the left and the value it is expected to be greater than or equal to on the right. func assertGreaterThanOrEqual(testCases: [TestPair]) { for testCase in testCases.pairsToTest { - XCTAssertGreaterThanOrEqual(testCase.leftClosure(), testCase.rightClosure(), file: testCase.file, line: testCase.line) + XCTAssertGreaterThanOrEqual( + testCase.leftClosure(), + testCase.rightClosure(), + testCase.message, + file: testCase.file, + line: testCase.line + ) } } @@ -149,7 +199,13 @@ public extension XCTestCase { /// - Parameter testCases: the cases to test, with the test value on the left and the expected value on the right. func assertEqual(testCases: [TestPair]) { for testCase in testCases.pairsToTest { - XCTAssertEqual(testCase.leftClosure(), testCase.rightClosure(), file: testCase.file, line: testCase.line) + XCTAssertEqual( + testCase.leftClosure(), + testCase.rightClosure(), + testCase.message, + file: testCase.file, + line: testCase.line + ) } } @@ -158,7 +214,14 @@ public extension XCTestCase { /// - Parameter accuracy: An expression of type `T`, where T conforms to `FloatingPoint`. This parameter describes the maximum difference between the test and control values for these values to be considered equal. func assertEqual(testCases: [TestPair], accuracy: T) { for testCase in testCases.pairsToTest { - XCTAssertEqual(testCase.leftClosure(), testCase.rightClosure(), accuracy: accuracy, file: testCase.file, line: testCase.line) + XCTAssertEqual( + testCase.leftClosure(), + testCase.rightClosure(), + accuracy: accuracy, + testCase.message, + file: testCase.file, + line: testCase.line + ) } } @@ -166,7 +229,13 @@ public extension XCTestCase { /// - Parameter testCases: the cases to test, with the test value on the left and the expected unequal value on the right. func assertNotEqual(testCases: [TestPair]) { for testCase in testCases.pairsToTest { - XCTAssertNotEqual(testCase.leftClosure(), testCase.rightClosure(), file: testCase.file, line: testCase.line) + XCTAssertNotEqual( + testCase.leftClosure(), + testCase.rightClosure(), + testCase.message, + file: testCase.file, + line: testCase.line + ) } } @@ -175,7 +244,14 @@ public extension XCTestCase { /// - Parameter accuracy: An expression of type `T`, where T conforms to `FloatingPoint`. This parameter describes the maximum difference between the test and control values for these values to be considered not equal. func assertNotEqual(testCases: [TestPair], accuracy: T) { for testCase in testCases.pairsToTest { - XCTAssertNotEqual(testCase.leftClosure(), testCase.rightClosure(), accuracy: accuracy, file: testCase.file, line: testCase.line) + XCTAssertNotEqual( + testCase.leftClosure(), + testCase.rightClosure(), + accuracy: accuracy, + testCase.message, + file: testCase.file, + line: testCase.line + ) } } @@ -195,8 +271,10 @@ public extension XCTestCase { tests: { pair, file, line in myCustomAssertion( pair.left, pair.right, - file: file, line: line // <-- ⚠️ this is important! + message: pair.message, + file: file, line: line // <-- ⚠️ file and line are important! ) + try youCanAlsoThrowErrorsInHere() // They will also get attributed to the correct line. } ) ``` @@ -209,7 +287,7 @@ public extension XCTestCase { do { try tests(testCase, testCase.file, testCase.line) } catch { - XCTFail("Failed: \(error)", file: testCase.file, line: testCase.line) + XCTFail("Failed: '\(error)' for test case '\(testCase.message)'", file: testCase.file, line: testCase.line) } } } diff --git a/Tests/TestCleanerTests/TestCleanerTests.swift b/Tests/TestCleanerTests/TestCleanerTests.swift index 0add3ff..9a63219 100644 --- a/Tests/TestCleanerTests/TestCleanerTests.swift +++ b/Tests/TestCleanerTests/TestCleanerTests.swift @@ -125,5 +125,41 @@ final class TestCleanerTests: XCTestCase { XCTAssertEqual(calledInputs, [1, 2]) } + + func testMessageLaziness_Pair() { + var callCount = 0 + func makeMessage() -> String { + callCount += 1 + return "the message" + } + let pair = Pair(1, 2, makeMessage()) + XCTAssertEqual(callCount, 0) + XCTAssertEqual(pair.message, "the message") + XCTAssertEqual(callCount, 1) + } + + func testMessageLaziness_fPair() { + var callCount = 0 + func makeMessage() -> String { + callCount += 1 + return "the message" + } + let pair = fPair(1, 2, makeMessage()) + XCTAssertEqual(callCount, 0) + XCTAssertEqual(pair.message, "the message") + XCTAssertEqual(callCount, 1) + } + + func testMessageLaziness_xPair() { + var callCount = 0 + func makeMessage() -> String { + callCount += 1 + return "the message" + } + let pair = xPair(1, 2, makeMessage()) + XCTAssertEqual(callCount, 0) + XCTAssertEqual(pair.message, "the message") + XCTAssertEqual(callCount, 1) + } } }