You can write an extension on Decimal
:
extension Decimal {
func isEqual(to other: Decimal, accuracy: Decimal) -> Bool {
abs(distance(to: other)).isLess(than: accuracy)
}
}
And then use it in your tests:
XCTAssertTrue(position.totalCost.isEqual(to: 3571.6, accuracy: 0.01))
This is likely good enough. However, to get better error messages in the case of a failing test would require writing an overload for XCTAssertEqual
, which is actually a bit tricky because elements of XCTest are not publicly available.
However, it is possible to approximate the behaviour:
Firstly, we need some plumbing to evaluate assertions, this can more or less be lifted straight from swift-corelibs-xctest.
import Foundation
import XCTest
internal enum __XCTAssertionResult {
case success
case expectedFailure(String?)
case unexpectedFailure(Swift.Error)
var isExpected: Bool {
switch self {
case .unexpectedFailure: return false
default: return true
}
}
func failureDescription() -> String {
let explanation: String
switch self {
case .success: explanation = "passed"
case .expectedFailure(let details?): explanation = "failed: \(details)"
case .expectedFailure: explanation = "failed"
case .unexpectedFailure(let error): explanation = "threw error \"\(error)\""
}
return explanation
}
}
internal func __XCTEvaluateAssertion(testCase: XCTestCase, _ message: @autoclosure () -> String = "", file: StaticString = #file, line: UInt = #line, expression: () throws -> __XCTAssertionResult) {
let result: __XCTAssertionResult
do {
result = try expression()
}
catch {
result = .unexpectedFailure(error)
}
switch result {
case .success: return
default:
let customMessage = message()
let description = customMessage.isEmpty ? result.failureDescription() : "\(result.failureDescription()) - \(customMessage)"
testCase.record(.init(
type: .assertionFailure,
compactDescription: description,
detailedDescription: nil,
sourceCodeContext: .init(
location: .init(filePath: String(describing: file), lineNumber: Int(line))
),
associatedError: nil,
attachments: [])
)
}
}
Now, for all of this to work, requires us to have access to the currently running XCTestCase
, inside a global XCTAssert*
function, which is not possible. Instead we can add our assert function in an extension.
extension XCTestCase {
func AssertEqual(
_ expression1: @autoclosure () throws -> Decimal,
_ expression2: @autoclosure () throws -> Decimal,
accuracy: @autoclosure () throws -> Decimal,
_ message: @autoclosure () -> String = "",
file: StaticString = #file,
line: UInt = #line
) {
__XCTEvaluateAssertion(testCase: self, message(), file: file, line: line) {
let lhs = try expression1()
let rhs = try expression2()
let acc = try accuracy()
guard lhs.isEqual(to: rhs, accuracy: acc) else {
return .expectedFailure("(\"\(lhs)\") is not equal to (\"\(rhs)\")")
}
return .success
}
}
}
All of which allows us to write our test cases as follows...
class MyTests: XCTestCase {
// etc
func test_decimal_equality() {
AssertEquals(position.totalCost, 3571.6, accuracy: 0.01)
}
}
And if the assertion fails, the test case will fail, with the message: ("3571.5") is not equal to ("3571.6")
at the correct line.
We also cannot call our function XCTAssertEquals, as this will override all the global assert functions.
You milage may vary, but once you have the plumbing in place, this allows you to write bespoke custom assertions for your test suite.