Swift:使用抛出的函数进行单元测试

时间:2016-02-10 19:44:54

标签: ios swift macos unit-testing

如何在Swift中编写单元测试,在调用可抛出的函数时传递有用的信息?

我真正希望能够做到的是:

class TestTestsTests: XCTestCase {

    func doFoo() throws -> String {
        // A complex operation that might throw in various places
        return "foo"
    }

    func doBar() throws -> String {
        // A complex operation that might throw in various places
        return "bar"
    }

    func testExample() throws {
        let foo = try doFoo()
        let bar = try doBar()
        XCTAssertNotEqual(foo, bar)
    }

}

理想情况下,单元测试运行器将停在发生未处理异常的行上,并让用户浏览堆栈和错误消息。不幸的是,将throws添加到测试函数会导致它被静默跳过(并且存在UI错误,使得它看起来好像测试仍在运行,并且获得与添加{之前相同的结果} {1}})。

当然也可以这样做:

throws

但现在失败并没有真正提供我们需要的背景。将func testExample() { let foo = try! doFoo() let bar = try! doBar() XCTAssertNotEqual(foo, bar) } 添加到throw,我们会收到doFoo之类的消息,该消息仅向我们提供fatal error: 'try!' expression unexpectedly raised an error: TestTestsTests.Error(): file /Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-700.1.101.15/src/swift/stdlib/public/core/ErrorType.swift, line 50内的行号,而不是testExample内的行号发生了错误。这似乎也让调试人员陷入了困境。 (无论是否启用了断点,都会在该行上单击“继续”,只返回同一行,并显示相同的错误消息),并阻止其他测试运行。

所以也许我们可以尝试这样的事情?

doFoo

这会按预期运行,但我们无法确定func testExample() { do { let foo = try doFoo() let bar = try doBar() XCTAssertNotEqual(foo, bar) } catch { XCTFail("\(error)") } } doFoo中的哪个引发了错误,也没有确定行号信息。至少我们可以得到错误消息,而不是阻止其他测试运行。

我可以继续,但缺点是我无法找到同时不破坏单元测试运行的方法(比如doBar),找出哪个函数抛出了错误,并获取错误信息 - 除非我做一些荒谬的事情,如:

try!

我仍然无法找出错误发生在func testExample() { var foo: String? = nil var bar: String? = nil do { foo = try doFoo() } catch { XCTFail("\(error)") return } do { bar = try doBar() } catch { XCTFail("\(error)") return } if let foo = foo, bar = bar { XCTAssertNotEqual(foo, bar) } } doFoo的位置。

这只是Swift中单元测试的悲惨状态,还是我错过了什么?

2 个答案:

答案 0 :(得分:1)

<块引用>

这按预期运行,但是我们无法确定 doFoo 或 doBar 中的哪个抛出了错误,也没有行号信息。

也许这只是一个老问题,或者我不理解您的问题,或者您只是在发表声明而不是提出问题。但是对于上面引用的语句,您可以将您喜欢的任何信息添加到 XCTest 断言中的消息中。 Swift 还提供以下文字:

#file          String   The name of the file in which it appears.
#line          Int      The line number on which it appears.
#column        Int      The column number in which it begins.
#function      String   The name of the declaration in which it appears. 


func testExample() {
    var foo: String? = nil
    var bar: String? = nil
    do {
        foo = try doFoo()
    } catch {
        XCTFail("try doFoo() failed on line: \(#line) in file: \(#file) with error: \(error)")
        return
    }
    do {
        bar = try doBar()
    } catch {
        XCTFail("try doBar() failed on line: \(#line) in file: \(#file) with error: \(error)")
        return
    }

    if let foo = foo, bar = bar {
        XCTAssertNotEqual(foo, bar)
    }
}

如果您真的想为它着迷,您可以向 doBar() 方法添加错误处理,并且该错误可以包含您想要的任何内部信息。 事实上……通过在方法中实现自己的错误,您甚至可能不需要在测试中将方法分成两个块,只需打印错误就足够了。您可以在错误消息中加入任何您喜欢的信息。

无论如何,我认为这是一个过时的问题,您可以从测试日志中获取您需要的所有信息——它们列出了所有失败的方法,甚至还有小箭头可以让您直接跳到失败的测试。然后他们突出显示失败的特定断言......从那里很容易判断在大多数情况下发生了什么。最坏的情况是您必须设置一两个断点并再次运行测试。

答案 1 :(得分:0)

您可以使用 ErrorLocalizedError 协议自行处理错误

enum Errors: Error, CustomStringConvertible {
    case foo_param_is_null
    case bar_param_is_null(paramIndex: Int)

    var description: String {
        switch self {
        case .foo_param_is_null:
            return "Param is null in foo"
        case .bar_param_is_null(let paramIndex):
            return "Param at index \(paramIndex) is null in bar"
        }
    }
}

func foo(_ param: Int) throws {
    guard param != 0 else {
        throw Errors.foo_param_is_null
    }
    print("foo = \(param)")
}

func bar(_ params: [Int]) throws {
    if let index = params.firstIndex(where: {$0 == 0}) {
        throw Errors.bar_param_is_null(paramIndex: index)
    }
    print("bar = \(params)")
}

do {
    try foo(1)
    try foo(0)
} catch {
    print("\(error)")
}

do {
    try bar([1,2,3])
    try bar([1,0,3])
} catch {
    print("\(error)")
}

结果:

foo = 1
Param is null in foo
bar = [1, 2, 3]
Param at index 1 is null in bar

如果您需要更多信息,您可以使用结构来定义错误和错误域。类似的东西:

struct FooBarError: Error, CustomStringConvertible {
    var string: String
    var context: Any?
    
    static func fooError() {
        FooBarError(string: "Foo Error")
    }
    
    static func barError(context: BarErrorContext) { FooBarError(string: "Bar Error", context: context)
    }
    
    var description: String {
        if let cox = context as? BarErrorContext {
            return "\(string) - paramIndex: \(ctx.paramIndex) - \(ctx.insidiousReason)"
        }
        return string
    }
}

注意:

正如@ibrust 所建议的,您可以将#function、#line 和其他特殊参数传递给您的错误初始化程序以提供此信息

do {
    try foo()
} catch {
    throw(BarFooError.foo(line: #line))
}

您也可以传播原始错误

do {
    try bar()
} catch {
    throw(BarFooError.bar(exception: error))
}

已编辑:

最后,您还可以在错误描述中使用 print(Thread.callStackSymbols),但此时,调试和测试之间存在混淆的风险。只是个人想法。