Skip to content

Instantly share code, notes, and snippets.

@nicholaslythall
Last active November 29, 2025 22:48
Show Gist options
  • Select an option

  • Save nicholaslythall/b57d02265992a77ed33b05be828f2c7d to your computer and use it in GitHub Desktop.

Select an option

Save nicholaslythall/b57d02265992a77ed33b05be828f2c7d to your computer and use it in GitHub Desktop.
Generic Equatable testing
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(&parameterCount), 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