如何在 swift 中测试包含 Task Async/await 的方法

给定以下包含 Task 的方法。

  • self.interactor 被嘲笑。

    
    func submitButtonPressed() {
    Task {
        await self.interactor?.fetchSections()
    }
    }


如何编写测试来验证 **fetchSections()** 是从该方法调用的?!

我的第一个想法是使用期望并等到它实现(在模拟代码中)。

但是新的 async/await 有什么更好的方法吗?
stack overflow How to test a method that contains Task Async/await in swift
原文答案
author avatar

接受的答案

理想情况下,正如您所暗示的,您的 interactor 将使用协议声明,以便您可以替换模拟以进行测试。然后查询模拟对象以确认调用了所需的方法。通过这种方式,您可以正确地将被测系统的范围限制为仅回答“是否调用了此方法?”的问题。

至于测试方法本身的结构,是的,这仍然是异步代码,因此需要异步测试。所以使用期望并等待它是正确的。您的应用程序使用 async/await 来表达异步性这一事实并不会神奇地改变这一点! (您可以通过编写一个创建 BOOL 谓词期望并等待它的实用方法来减少它的冗长性。)


答案:

作者头像

我在这篇文章中回答了一个类似的问题: https://stackoverflow.com/a/73091753/2077405

基本上,给定这样定义的代码:

class Owner{
   let dataManager: DataManagerProtocol = DataManager()
   var data: String? = nil

   init(dataManager: DataManagerProtocol = DataManager()) {
      self.dataManager = dataManager
   }

   func refresh() {
        Task {
            self.data = await dataManager.fetchData()
        }
    }
}

DataManagerProtocol 定义为:

protocol DataManagerProtocol {
   func fetchData() async -> String
}

可以定义模拟/假实现:

class MockDataManager: DataManagerProtocol {
    func fetchData() async -> String {
       "testData"
    }
}

实现单元测试应该是这样的:

...
func testRefreshFunctionFetchesDataAndPopulatesFields() {        
  let expectation = XCTestExpectation(
    description: "Owner fetches data and updates properties."
  )

  let owner = Owner(mockDataManager: DataManagerProtocol())

  // Verify initial state
  XCTAssertNil(owner.data)

  owner.refresh()

  let asyncWaitDuration = 0.5 // <= could be even less than 0.5 seconds even
  DispatchQueue.main.asyncAfter(deadline: .now() + asyncWaitDuration) {
    // Verify state after
    XCTAssertEqual(owner.data, "testData")

    expectation.fulfill()
  }

  wait(for: [expectation], timeout: asyncWaitDuration)
}
...

希望这有意义吗?

作者头像

我不知道您是否已经找到了问题的解决方案,但这是我对面临相同问题的其他开发人员的贡献。

我的情况和你一样,我通过使用Combine通知被测类该方法被调用解决了这个问题。

假设我们有这个方法可以测试:

func submitButtonPressed() {
    Task {
        await self.interactor?.fetchSections()
    }
}

我们应该从模拟交互开始:

import Combine

final class MockedInteractor: ObservableObject, SomeInteractorProtocol {
    @Published private(set) var fetchSectionsIsCalled = false

    func fetchSection async {
        fetchSectionsIsCalled = true
        // Do some other mocking if needed
    }
}

现在我们有了模拟的交互器,我们可以开始编写单元测试:

import XCTest
import Combine
@testable import YOUR_TARGET

class MyClassTest: XCTestCase {
    var mockedInteractor: MockedInteractor!
    var myClass: MyClass!
    private var cancellable = Set<AnyCancellable>()

    override func setUpWithError() throws {
        mockedInteractor = .init()
        // the interactor should be injected
        myClass = .init(interactor: mockedInteractor)
    }

    override func tearDownWithError() throws {
        mockedInteractor = nil
        myClass = nil
    }

    func test_submitButtonPressed_should_callFetchSections_when_Always(){
        //arrage
        let methodCallExpectation = XCTestExpectation()

        interactor.$fetchSectionsIsCalled
            .sink { isCalled in
                if isCalled {
                    methodCallExpectation.fulfill()
                }
            }
            .store(in: &cancellable)

        //acte
        myClass.submitButtonPressed()
        wait(for: [methodCallExpectation], timeout: 1)

        //assert
        XCTAssertTrue(interactor.fetchSectionsIsCalled)
    }
作者头像

这里 (@andy) 提出了一种涉及注入 Task 的解决方案。有一种方法可以通过 func 执行返回 Task 的任务并允许对 await value 进行测试。
(我并不热衷于更改可测试类以适应测试(返回 Task ),但它允许在没有 async 的情况下测试 NSPredicate 或设置一些任意期望时间(这只是闻起来)) .

@discardableResult
func submitButtonPressed() -> Task<Void, Error> {
    Task { // I'm allowed to omit the return here, but it's returning the Task
        await self.interactor?.fetchSections()
    }
}

// Test
func testSubmitButtonPressed() async throws {

    let interactor = MockInteractor()

    let task = manager.submitButtonPressed()

    try await task.value

    XCTAssertEqual(interactor.sections.count, 4)
}