调用异步方法的方法的单元测试

8

假设我有以下代码:

func reset() {
    initializeAnythingElse() {
        // AnythingElse
    }

    initializeHomeData() {
        // HomeData
    }
}

func initializeHomeData(callback: @escaping (()-> Void)) {
    getHomeConfig() {
        callback()
    }
}

func initializeAnythingElse(callback: @escaping (()-> Void)) {
    getAnythingElse() {
        callback()
    }
}

我希望为那段代码编写单元测试。对于initializeHomeDatainitializeAnythingElse,我可以编写如下单元测试:

func testInitializeHomeData() {
    let successExpectation = expectation(description: "")

    sut.initializeHomeData {
        successExpectation.fulfill()
    }

    waitForExpectations(timeout: 1.0, handler: nil)

    // Validation goes here
}

func testInitializeAnythingElse() {
    let successExpectation = expectation(description: "")

    sut.initializeAnythingElse {
        successExpectation.fulfill()
    }

    waitForExpectations(timeout: 1.0, handler: nil)

    // Validation goes here
}

我的问题是,如何测试reset()方法?我是否应该在testReset()中调用它们,像这样:testReset()
func testReset() {
    testInitializeHomeData()
    testInitializeAnythingElse()
}

但我认为这并不是适当的实现方式。


这是一个非常重要的问题!经过长时间的搜索,我终于找到了它。 - BharathRao
3个回答

10

你说得对。要测试reset,需要调用reset而不是它的内部方法。

话虽如此,reset目前写得让它无法被测试。你之所以能够轻松地测试其他独立方法,是因为callback参数接受了两个回调函数。

我建议你按以下方式重新编写reset,允许两个可选的回调函数:

typealias Callback = () -> ()

func reset(
    homeDataCallback: @escaping Callback? = nil,
    anythingElseCallback: @escaping Callback? = nil) {
    initializeAnythingElse() {
       anythingElseCallback?()
    }
    initializeHomeData() {
       homeDataCallback?()
    }
}

请注意,此更改允许您在异步方式下在这两个内部调用完成时收到通知。

现在,您的测试方法需要考虑某种同步原语,因为从逻辑上讲,只有当home数据和其他内容都完成并调用了它们的回调函数时,重置才算完成。

有许多实现这一点的方法,但我将向您展示使用信号量的一种方法:

func testReset() {

    let expectation = expectation(description: "reset() completes within some duration")

    // some mechanism to synchronize concurrent tasks
    // I am using a semaphore
    let s = DispatchSemaphore(value: 0)

    let homeCallback: Callback =  {
        s.signal() // signals the completion of home data init
    }

    let anythingElseCallback: Callback = {
        s.signal() // signals the completions of anything else setup
    }

    // call your reset method as part of the test
    reset(homeDataCallback: homeCallback, anythingElseCallback: anythingElseCallback)

    // we know we need to wait for two things to complete
    // init home data and anything else, so do that
    s.wait()
    s.wait()

    // at this step, reset's internal async methods
    // have completed so we can now
    // fulfill the expectation
    expectation.fulfill()

}

请注意,所有这些更改都是为了让您能够测试reset调用。您的函数签名允许您在现有代码中将reset()编写为当前版本,因为它具有可选参数,这些参数都设置为默认值nil。


嘿 @Benzi,谢谢你的回答。但是对于那个双重 s.wait() 的行,有没有其他的写法呢?如果我有多于两个回调函数怎么办? - mrjimoy_05
1
使用信号量路线,如果您有n个回调函数,则需要调用wait() n次。 - Benzi

0

reset 方法中添加回调函数:

func reset(callback: @escaping (()-> Void)) {
    var callbacksCount = 0

    func tryCallback() {
        if callbacksCount == 2 {
            callback()
        }
    }

    initializeAnythingElse() {
        callbacksCount += 1
    }

    initializeHomeData() {
        callbacksCount += 1    
    }
}

并且像其他方法一样测试它。

您还可以使调用顺序执行,平行执行似乎不值得更复杂的代码优化:

func reset(callback: @escaping (()-> Void)) {
    initializeAnythingElse() {
        initializeHomeData() {
            callback()
        }
    }
}

0

请查看 XCTestExpectation

这是异步测试的推荐 API。步骤如下:

  1. 创建一个 XCTestExpectation 实例

  2. 调用您的异步方法

  3. 调用 waitForExpectations(timeout: timeout, handler: handler)

  4. 第 3 步提供了异步调用完成的等待时间。每当您的异步方法完成时,在 XCTestExpectation 实例上调用 .fulfill()

供参考 -

Xcode 6 中的 XCTest 和异步测试

将上面答案中的代码粘贴到此处,以防链接在以后停止工作 -

func testFetchNews() {
    let expectation = self.expectationWithDescription("fetch posts")

    Post.fetch(.Top, completion: {(posts: [Post]!, error: Fetcher.ResponseError!) in
        XCTAssert(true, "Pass")
        expectation.fulfill()
    })

    self.waitForExpectationsWithTimeout(5.0, handler: nil)
}

希望这能有所帮助。

请注意,您一次只能有一个未完成的期望,因此请注意嵌套。 - MattD

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接