8 min read

iOS 单元测试框架 XCTest (二)Assert

iOS测试框架XCTest提供了许多断言,以下是一些常见的断言及其使用例子:

Bool 断言

  • XCTAssert:断言一个条件为true
XCTAssert(true, "This should always pass")
  • XCTAssertTrue:断言一个条件为true
let a = 5
let b = 10
XCTAssertTrue(a < b, "a should be less than b")
  • XCTAssertFalse:断言一个条件为false
let a = 5
let b = 10
XCTAssertFalse(a > b, "a should not be greater than b")

相等和不等断言

值比较

下面两个是值的相等比较,如基础类型Int、String、Array、Struct等。

  • XCTAssertEqual:断言两个对象相等
  • XCTAssertNotEqual:断言两个对象不相等
let a = "Hello"
let b = "Hello"
XCTAssertEqual(a, b, "These two strings should be equal")

let c = "Hello"
let d = "World"
XCTAssertNotEqual(c, d, "These two strings should not be equal")

还有两个泛型变种,当T类型遵循 FloatingPoint 或者 Numeric 时,可以指定精度 accuracy,当有两者都有对应的 XCTAssertNotEqual断言。

func XCTAssertEqual<T>(
    _ expression1: @autoclosure () throws -> T,
    _ expression2: @autoclosure () throws -> T,
    accuracy: T,
    _ message: @autoclosure () -> String = "",
    file: StaticString = #filePath,
    line: UInt = #line
) where T : FloatingPoint

func XCTAssertEqual<T>(
    _ expression1: @autoclosure () throws -> T,
    _ expression2: @autoclosure () throws -> T,
    accuracy: T,
    _ message: @autoclosure () -> String = "",
    file: StaticString = #filePath,
    line: UInt = #line
) where T : Numeric

下面是一个 FloatingPoint值的比较

let calculator = Calculator()
let result = calculator.calculateSquareRoot(9)
XCTAssertEqual(result, 3.0, accuracy: 0.01, "Square root of 9 should be 3")

对象比较

下面这两个函数通常用于测试对象的引用相等性,即两个对象是否指向同一块内存

  • XCTAssertIdentical:断言两个对象是同一个对象,即它们的内存地址相同。
  • XCTAssertNotIdentical:断言两个对象不是同一个对象,即它们的内存地址不同。

大小比较断言

用于比较可比较类型(遵循 Comparable 协议)的值的断言函数

  • XCTAssertGreaterThan:大于比较
  • XCTAssertGreaterThanOrEqual:大于等于比较
  • XCTAssertLessThan:小于比较
  • XCTAssertLessThanOrEqual:小于等于比较

例如我们验证升序排序算法

func testSortAscendingOrder() {
    let numbers = [2, 1, 3]
    let sortedNumbers = numbers.sorted()
    XCTAssertLessThan(sortedNumbers[0], sortedNumbers[1], "The first number should be less than the second number")
}

空值和非空值断言

  • XCTAssertNil:断言一个对象为nil
let a: String? = nil
XCTAssertNil(a, "a should be nil")
  • XCTAssertNotNil:断言一个对象不为nil
let a: String? = "Hello"
XCTAssertNotNil(a, "a should not be nil")
  • XCTUnwrap:检查可选值是否不为 nil,如果不为 nil,则返回已解包的值。
let optionalValue: String? = "Hello, world!"
let unwrappedValue = try XCTUnwrap(optionalValue)

XCTAssertEqual(unwrappedValue, "Hello, world!")

抛出错误和不抛出错误断言

  • XCTAssertThrowsError:断言一个表达式抛出了一个错误

完整定义如下:

func XCTAssertThrowsError<T, E>(
    _ expression: @autoclosure () throws -> T,
    _ message: @autoclosure () -> String = "",
    file: StaticString = #filePath,
    line: UInt = #line,
    _ errorHandler: (E) -> Void = { _ in }
) where E : Error, E : Equatable

该函数有两个参数要注意:

  • expression:要测试的表达式,类型为 () throws -> T,表示该表达式可能抛出一个错误。
  • errorHandler:可选的错误处理函数,用于检查抛出的错误是否符合预期,类型为 (E) -> Void,其中 E 是期望的错误类型,并且必须遵循 ErrorEquatable 协议。

下面看一个例子:

func divide(_ a: Int, by b: Int) throws -> Int {
    guard b != 0 else {
        throw NSError(domain: "Division by zero", code: 1, userInfo: nil)
    }
    return a / b
}

XCTAssertThrowsError(try divide(5, by: 0), "Divide by zero error should be thrown") { error in
    XCTAssertEqual(error.localizedDescription, "Division by zero")
}
  • XCTAssertNoThrow:测试一个表达式是否不会抛出错误
func testAddingNumbers() {
    let calculator = Calculator()
    let result = XCTAssertNoThrow(try calculator.add(2, to: 3), "Adding two numbers should not throw an error")
    XCTAssertEqual(result, 5, "The result should be 5")
}

无条件的测试失败

  • XCTFail:表示该测试总是失败
func testAlwaysFail() {
    XCTFail("This test always fails")
}

通常情况下,XCTFail 用于表示某些预期的条件不满足,或者测试的某些部分出现了意外的错误。如果测试代码执行到 XCTFail,该测试将被标记为“失败”,并在测试报告中显示错误消息。

需要注意的是,XCTFail 可以在任何测试函数中使用,但应该避免在测试代码中使用过多的 XCTFail,因为它们可能会使测试报告变得混乱和难以理解。通常来说,最好使用其他的断言函数来测试特定的条件和预期结果,只在必要时使用 XCTFail 表示测试失败。

预期中的错误断言

XCTExpectFailure 是 XCTest 框架中的一个函数,用于标记一个测试用例或测试代码中的某个部分为“预期失败”。该函数的定义如下:

func XCTExpectFailure(
    _ failureReason: String? = nil,
    options: XCTExpectedFailure.Options = .init()
)

func XCTExpectFailure<R>(
    _ failureReason: String? = nil,
    options: XCTExpectedFailure.Options = .init(),
    failingBlock: () throws -> R
) rethrows -> R

其中 failingBlock:需要标记为“预期失败”的测试代码块。

使用 XCTExpectFailure 可以将测试结果标记为“预期失败”。这在测试代码中存在某些已知的问题或者限制时非常有用。例如,如果某个测试用例在特定的条件下经常失败,但是在其他情况下能够正常运行,可以使用 XCTExpectFailure 标记该测试用例的失败部分为“预期失败”,以避免测试结果被错误地标记为“实际失败”。

下面是一个使用 XCTExpectFailure 的示例:

func testDivisionByZero() {
    let calculator = Calculator()
    XCTExpectFailure("Dividing by zero should throw an error") {
        XCTAssertThrowsError(try calculator.divide(10, by: 0), "The error should be divideByZero")
    }
}

在这个示例中,XCTExpectFailure 用于标记测试中的某个部分是“预期失败”的。具体来说,它标记了 XCTAssertThrowsError 的某个断言应该是“预期失败”的,因为该断言将抛出一个错误,而该错误是由 calculator.divide(10, by: 0) 导致的,而该表达式是除以零的情况,因此应该抛出一个 CalculatorError.divideByZero 的错误。如果未抛出该错误,该断言将导致测试失败,并在测试报告中显示错误消息 "The error should be divideByZero"。但是,由于该部分代码已经被标记为“预期失败”,因此测试结果将被标记为“预期失败”,而不是“实际失败”。

需要注意的是,XCTExpectFailure 只适用于测试中已经预期测试某些部分会失败的情况。如果测试代码中存在未处理的错误或异常,测试框架将自动将其标记为“实际失败”,而不是“预期失败”。因此,在编写测试代码时,需要仔细考虑预期的测试结果,并使用适当的断言函数来测试预期的条件和结果,以便测试框架能够正确地处理测试结果。

增强的 XCTExpectFailure
Xcode 13.0 增加了两个重载函数,增强了 XCTExpectFailure 的用法。

func XCTExpectFailure(
    _ failureReason: String? = nil,
    enabled: Bool? = nil,
    strict: Bool? = nil,
    issueMatcher: ((XCTIssue) -> Bool)? = nil
)

func XCTExpectFailure<R>(
    _ failureReason: String? = nil,
    enabled: Bool? = nil,
    strict: Bool? = nil,
    failingBlock: () throws -> R,
    issueMatcher: ((XCTIssue) -> Bool)? = nil
) rethrows -> R

下面是一个使用了所有可选参数的示例:

func testDivisionByZero() throws {
    let calculator = Calculator()
    try XCTContext.runActivity(named: "Test division by zero") { _ in
        XCTExpectFailure("Dividing by zero should throw an error",
                         enabled: false,
                         strict: true
        ) {
            XCTAssertThrowsError(
                try calculator.divide(10, by: 0),
                "The error should be divideByZero"
            )
        } issueMatcher: { issue in
            issue.compactDescription.contains("divideByZero")

        }
}

下面是这些可选参数的含义:

enabled 被设置为 false,这意味着该测试用例的“预期失败”标记将被禁用。这将导致该测试用例的所有断言都将被视为正常的测试断言,并将产生“实际失败”测试结果。

strict 被设置为 true,这意味着测试框架将在处理“预期失败”测试结果时更加严格。具体来说,如果测试代码中存在未标记为“预期失败”的错误或异常,测试框架将抛出一个致命错误,而不是将测试结果标记为“实际失败”。这有助于确保测试代码在预期的条件下失败,并防止测试结果被错误地标记为“实际失败”。

issueMatcher:可选的闭包,用于自定义“预期失败”测试结果的匹配规则。默认值为 nil,这意味着测试框架将使用默认的匹配规则。

跳过测试

XCTSkip即可跳过当前测试用例。跳过测试的使用场景一般是当前的代码没有测试通过,但是我目前没有时间来修复bug,先做其他的测试。

以前的方法是将用例禁用到,可以用快捷键Cmd + Shit + >或者菜单Product,进入 编辑Test Plan界面。

下面两个是增加了条件的跳过测试方法。

  • XCTSkipIf:跳过符合条件的测试

func testThatViewAppearsCorrectOnIPad() throws {
    
    try XCTSkipIf(UIDevice.current.userInterfaceIdiom != .pad, "Skipping test. Test only applicable for iPad devices")
    
    let view = CustomView(frame: CGRect(x: 0, y: 0, width: 320, height: 540))
    view.adjustSize()
    
    XCTAssertEqual(view.frame.size, CGSize(width: 500.0, height: 500.0))
}
  • XCTSkipUnless:除非符合条件,否则都跳过测试

func testThatViewAppearsCorrectOnIPad() throws {
    
    try XCTSkipUnless(UIDevice.current.userInterfaceIdiom == .pad, "Skipping test. Test only applicable for iPad devices")
    
    let view = CustomView(frame: CGRect(x: 0, y: 0, width: 320, height: 540))
    view.adjustSize()
    
    XCTAssertEqual(view.frame.size, CGSize(width: 500.0, height: 500.0))
}

总结

以上是一些常见的XCTest断言及其使用例子,XCTest还提供了许多其他断言来帮助测试不同类型的对象和条件。