How to handle concurrency errors in unit tests 如何处理单元测试中的并发错误

How to handle concurrency errors in unit tests 如何处理单元测试中的并发错误

温馨提示:本文最后更新于2025-02-10 10:28:59,某些文章具有时效性,若有错误或已失效,请在下方留言

Both Swift Testing and XCTest give us three main options for handling errors in tests: we can consider them immediate failures, we can handle them internally and make our own assertions, or we can consider them expected.
Swift Testing 和 XCTest 都为我们提供了处理测试中错误的三个主要选项:我们可以将它们视为即时故障,我们可以在内部处理它们并做出自己的断言,或者我们可以认为它们是意料之中的。

First, let’s look at the simplest solution in both frameworks, which is considering thrown errors to be test failures. This is done by marking your test case with throws and not handling the error – the failing test error will propagate upwards to Swift Testing and XCTest, which will fail that test.
首先,让我们看看两个框架中最简单的解决方案,即将引发的错误视为测试失败。这是通过用 throws 标记你的测试用例而不是处理错误来完成的——失败的测试错误将向上传播到 Swift Testing 和 XCTest,这将使该测试失败。

As an example, we might be working with an error type such as this one:
例如,我们可能正在使用如下错误类型:

enum LoadError: Error {
    case notEnoughData
    case tooMuchData
}

In Swift Testing, we could write a test to call a method that might throw one of those errors:
在 Swift 测试中,我们可以编写一个测试来调用一个可能抛出以下错误之一的方法:

@Test("Loading view model names")
func loadNames() async throws {
    let viewModel = ViewModel()
    try await viewModel.loadNames()
    #expect(viewModel.names.isEmpty == false, "Names should be full of values.")
}

There is no code in there to handle the error being thrown, but that’s okay: if loadNames() completes successfully then the expectation will be checked as normal, but if it throws an error then the test will be failed with a message such as Caught error: .notEnoughData.
那里没有代码来处理抛出的错误,但没关系:如果 loadNames() 成功完成,则将照常检查预期,但如果它抛出错误,则测试将失败,并显示一条消息,例如 Caught error: .notEnoughData

With XCTest, the approach is identical – mark your test with throws and let any errors propagate upwards, like this:
使用 XCTest,方法是相同的 – 用 throw 标记你的测试,并让任何错误向上传播,就像这样:

final class DataHandlingTests: XCTestCase {
    func test_loadNames() async throws {
        let viewModel = ViewModel()
        try await viewModel.loadNames()
        XCTAssertFalse(viewModel.names.isEmpty == false, "No data was loaded.")
    }
}

That will perform identically to Swift Testing: either the method completes successfully and the assertion is checked, or an error is thrown and the test fails with a simple message.
这将与 Swift 测试相同:要么方法成功完成并检查断言,要么抛出错误并且测试失败并显示一条简单的消息。

While this approach is definitely appealing for its simplicity, I’m not a big fan of it because the error doesn’t say exactly what was expected.
虽然这种方法因其简单性而具有吸引力,但我并不是它的忠实粉丝,因为错误并没有完全说明预期的结果。

Swift Testing does offer a better solution here, though, which is to add a retroactive conformance to CustomTestStringConvertible in your test target. To be clear, this should be in your test target not in your main production code.
不过,Swift Testing 确实在这里提供了更好的解决方案,即在测试目标中添加对 CustomTestStringConvertible 的追溯一致性。需要明确的是,这应该在你的测试目标中,而不是在你的主要产品代码中。

For example, we could clarify what our two load errors actually mean like this:
例如,我们可以像这样澄清我们的两个加载误差的实际含义:

extension LoadError: @retroactive CustomTestStringConvertible {
    public var testDescription: String {
        switch self {
        case .notEnoughData:
            "At least three names should be loaded."
        case .tooMuchData:
            "No more than 1000 names are supported."
        }
    }
}

XCTest does not have an equivalent for this feature.
XCTest 没有此功能的等效项。

However, for both XCTest and Swift Testing an alternative is to handle the error internally and issue your own message as needed.
但是,对于 XCTest  Swift Testing,另一种方法是在内部处理错误并根据需要发出您自己的消息。

For Swift Testing this is done using Issue.record(), passing in the comment you want to flag up and also optionally also the error itself:
对于 Swift 测试,这是通过 Issue.record() 完成的,传入你想要标记的注释,也可以选择传入错误本身:

@Test("Loading view model names")
func loadNames() async {
    let viewModel = ViewModel()

    do {
        try await viewModel.loadNames()
        #expect(viewModel.names.isEmpty == false, "Names should be full of values.")
    } catch {
        Issue.record(error, "Between 3 and 1000 names are supported.")
    }
}

In XCTest the equivalent is catching the error manually, then calling XCTFail() like this:
在 XCTest 中,等效的方法是手动捕获错误,然后像这样调用 XCTFail() :

final class DataHandlingTests: XCTestCase {
    func test_loadNames() async {
        let viewModel = ViewModel()

        do {
            try await viewModel.loadNames()
            XCTAssertFalse(viewModel.names.isEmpty == false, "No data was loaded.")
        } catch {
            XCTFail("Between 3 and 1000 names are supported.")
        }
    }
}

Tip: For both Swift Testing and XCTest, handling the errors inside the text means the test itself is no longer marked throws.
提示: 对于 Swift Testing 和 XCTest 来说,处理文本中的错误意味着测试本身不再被标记为 throws

If the error is expected to be thrown – if your test is checking that an error is thrown when invalid data is provided, for example – then you’ll need to take a slightly different approach.
如果预期会引发错误 – 例如,如果您的测试正在检查在提供无效数据时是否引发错误 – 那么您将需要采用略有不同的方法。

With Swift Testing, you can run some code and expect that a specific error type is thrown:
使用 Swift Testing,您可以运行一些代码并预期会抛出特定的错误类型:

@Test("Loading view model names")
func loadNames() async {
    let viewModel = ViewModel()

    await #expect(throws: LoadError.self, "At least three names should be loaded.", performing: viewModel.loadNames)
}

That will fail the test if LoadError isn’t thrown. You can even expect a specific error case to be thrown by using LoadError.notEnoughData rather than LoadError.self:
如果未引发 LoadError*,*则测试将失败。您甚至可以通过使用 LoadError.notEnoughData 而不是 LoadError.self 来引发特定的错误情况:

@Test("Loading view model names")
func loadNames() async {
    let viewModel = ViewModel()

    await #expect(throws: LoadError.notEnoughData, "At least three names should be loaded.", performing: viewModel.loadNames)
}

In XCTest things are a little trickier: there is an XCTAssertThrowsError() function, but it doesn’t support async functions. So, instead we should implement do/catch inside the test, then use XCTFail() to report a test failure if the code didn’t throw:
在 XCTest 中,事情有点棘手:有一个 XCTAssertThrowsError() 函数,但它不支持异步函数。因此,我们应该在测试中实现 do/catch,然后在代码没有抛出时使用 XCTFail() 报告测试失败:

final class DataHandlingTests: XCTestCase {
    func test_loadNames() async throws {
        let viewModel = ViewModel()

        do {
            try await viewModel.loadNames()
            XCTFail("This should fail to load.")
        } catch {
            // doing nothing means the test passed
        }
    }
}

Swift Testing provides one extra useful option, which is the withKnownIssue() function. This expects an error to be thrown and will in fact fail your test if the error isn’t thrown.
Swift Testing 提供了一个额外的有用选项,即 withKnownIssue() 函数。这期望引发错误,如果未引发错误,则实际上会使测试失败。

Here’s how it looks:  这是它的外观:

@Test("Loading view model names")
func loadNames() async {
    let viewModel = ViewModel()

    await withKnownIssue("Names can sometimes come back with too few values") {
        try await viewModel.loadNames()
        #expect(viewModel.names.isEmpty == false, "Names should be full of values.")
    }
}

This has a variation that is particularly useful while you’re debugging networking code, which is the isIntermittent parameter – setting that to true will tell Swift to pass the test if no error is thrown, but mark an expected failure if an error is thrown, so it’s a nice middle ground while you’re tackling a problem.
这有一个变体,在调试网络代码时特别有用,那就是 isIntermittent 参数 —— 如果 true 没有抛出错误,则告诉 Swift 通过测试,但如果抛*出错误,*则标记为预期失败,因此在解决问题时,这是一个很好的中间地带。

We could switch our current code over to intermittent failures like this:
我们可以将当前代码切换到间歇性故障,如下所示:

@Test("Loading view model names")
func loadNames() async {
    let viewModel = ViewModel()

    await withKnownIssue("Names can sometimes come back with too few values", isIntermittent: true) {
        try await viewModel.loadNames()
        #expect(viewModel.names.isEmpty == false, "Names should be full of values.")
    }
}
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享