Last active
November 29, 2025 22:48
-
-
Save nicholaslythall/b57d02265992a77ed33b05be828f2c7d to your computer and use it in GitHub Desktop.
Generic Equatable testing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| protocol Randomizable { | |
| static func randomized() -> Self | |
| } | |
| /// - returns: An array of producer functions that will return a value `T`, using the given `factory`, where each producer invokes | |
| /// `factory` with a different combination of parameters. When used to construct a test matrix, the number of returned producers is enough to | |
| /// verify that the equality implementation of `T` correctly considers each input parameter. | |
| func makeTestCasesForEquality<T, each U: Randomizable & Equatable>(factory: @escaping (repeat each U) -> T) -> [() -> T] { | |
| /// - returns: two different random values of the given `type` | |
| func twoDifferentValues<R : Randomizable & Equatable>(of type: R.Type) -> (a: R, b: R) { | |
| let a = R.randomized() | |
| var b: R | |
| for _ in 0..<100 { | |
| b = R.randomized() | |
| if b != a { | |
| return (a, b) | |
| } | |
| } | |
| preconditionFailure("Could not produce two different values of type \(R.self)") | |
| } | |
| /// - returns: Returns the current value of `i` and then increments it by `1`, replicating `i++` | |
| func getAndIncrement(_ i: inout Int) -> Int { | |
| defer { i += 1 } | |
| return i | |
| } | |
| // Count the number of parameters, and create a tuple matrix of the parameter's index, | |
| // and two different values for each parameter, e.g: | |
| // ( | |
| // (0, (a0, b0)), | |
| // (1, (a1, b1)), | |
| // ... | |
| // ) | |
| var parameterCount = 0 | |
| let parameterMatrix = (repeat (index: getAndIncrement(¶meterCount), values: twoDifferentValues(of: (each U).self))) | |
| // Create a tuple of functions that will select either the 'a' or 'b' value for each parameter, | |
| // depending on whether its index is 'flipped' | |
| let parameterSelectors = (repeat { flippedIndex in | |
| let (thisIndex, (a, b)) = (each parameterMatrix) | |
| return flippedIndex != thisIndex ? a : b | |
| }) | |
| // Create two control test cases where all parameters are 'a' or all parameters are 'b' | |
| var testCaseProducers: [() -> T] = [ | |
| { factory(repeat (each parameterMatrix).values.a) }, | |
| { factory(repeat (each parameterMatrix).values.b) }, | |
| ] | |
| // For each parameter, create a test case where that one parameter is flipped to 'b' and all others are 'a' | |
| testCaseProducers += (0..<parameterCount).map { indexOfParameterToAlter in | |
| return { return factory(repeat (each parameterSelectors)(indexOfParameterToAlter)) } | |
| } | |
| return testCaseProducers | |
| } | |
| // Usage | |
| struct Foo : Equatable { | |
| let bar: Bool | |
| let baz: Bool | |
| let qux: Bool | |
| static func == (lhs: Foo, rhs: Foo) -> Bool { | |
| return lhs.bar == rhs.bar | |
| && lhs.baz == rhs.baz | |
| // opps! | |
| // && lhs.qux == rhs.qux | |
| } | |
| } | |
| let testCasees = makeTestCasesForEquality(Foo.init) | |
| for lhs in testCases.enumerated { | |
| for rhs in testCases.enumerated { | |
| if lhs.offset == rhs.offset { | |
| XCTAssertEqual(lhs.element(), rhs.element()) | |
| } else { | |
| XCTAssertNotEqual(lhs.element(), rhs.element()) | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment