ios - 关于使用 xctest 在 swift 和 ios 中模拟单例的查询?

标签 ios swift unit-testing

这不是关于我们是否应该使用单例的问题。而是 mock 单例相关。 这只是一个示例,因为我正在阅读有关模拟单例很难的文章。所以我想让我试一试。 我可以模拟它,但不确定这是正确的方法吗?

protocol APIManagerProtocol {
    static var sharedManager: APIManagerProtocol {get set}
    func doThis()

}

class APIManager: APIManagerProtocol {
    static var sharedManager: APIManagerProtocol = APIManager()
    private init() {
    }

    func doThis() {

    }
}

class ViewController: UIViewController {

    private var apiManager: APIManagerProtocol?
    override func viewDidLoad() {

    }
    convenience init(_ apimanager: APIManagerProtocol){
        self.init()
        apiManager = apimanager
    }

    func DoSomeRandomStuff(){
        apiManager?.doThis()
    }
}

import Foundation
@testable import SingleTonUnitTesting

class MockAPIManager: APIManagerProtocol {
    static var sharedManager: APIManagerProtocol = MockAPIManager()

    var isdoThisCalled = false

    func doThis(){
        isdoThisCalled = true
    }
    private init(){

    }
}

class ViewControllerTests: XCTestCase {

    var sut: ViewController?
    var mockAPIManager: MockAPIManager?

    override func setUp() {
        mockAPIManager = MockAPIManager.sharedManager as? MockAPIManager
        sut = ViewController(mockAPIManager!)
    }


    func test_viewController_doSomeRandomStuffs(){
        sut?.DoSomeRandomStuff()
        XCTAssertTrue(mockAPIManager!.isdoThisCalled)
    }

    override func tearDown() {
        sut = nil
        mockAPIManager = nil
    }


}

最佳答案

基本思路是对的:避免在整个代码中直接重复引用单例,而是注入(inject)符合协议(protocol)的对象。

不太正确的是,您正在测试 MockAPIManager 类的内部内容。模拟只是为了服务于更广泛的目标,即测试您的业务逻辑(没有外部依赖性)。因此,理想情况下,您应该测试 APIManagerProtocol 公开的内容(或它的一些逻辑结果)。

那么,让我们具体一点:例如,假设您的 API 有一些方法可以从 Web 服务中检索用户的年龄:

public protocol APIManagerProtocol {
    func fetchAge(for userid: String, completion: @escaping (Result<Int, Error>) -> Void)
}

(注意,顺便说一句,static 单例方法不属于协议(protocol)。它是 API 管理器的实现细节,而不是协议(protocol)的一部分。没有 Controller 获得注入(inject)的管理器将永远需要自己调用 shared/sharedManager。)

让我们假设您的 View Controller (或者更好的是,它的 View 模型/展示器)有一个方法来检索年龄并创建一个适当的消息以显示在 UI 中:

func buildAgeMessage(for userid: String, completion: @escaping (String) -> Void) {
    apiManager?.fetchAge(for: userid) { result in
        switch result {
        case .failure:
            completion("Error retrieving age.")

        case .success(let age):
            completion("The user is \(age) years old.")
        }
    }
}

然后 API 管理器 mock 将实现该方法:

class MockAPIManager: APIManagerProtocol {
    func fetchAge(for userid: String, completion: @escaping (Result<Int, Error>) -> Void) {
        switch userid {
        case "123":
            completion(.success(42))

        default:
            completion(.failure(APIManagerError.notFound))
        }
    }
}

然后您可以使用模拟的 API 而不是实际的网络服务来测试构建要在您的 UI 中显示的字符串的逻辑:

class ViewControllerTests: XCTestCase {
    var viewController: ViewController?

    override func setUp() {
        viewController = ViewController(MockAPIManager())
    }

    func testSuccessfulAgeMessage() {
        let e = expectation(description: "testSuccessfulAgeMessage")
        viewController?.buildAgeMessage(for: "123") { string in
            XCTAssertEqual(string, "The user is 42 years old.")
            e.fulfill()
        }
        waitForExpectations(timeout: 1)
    }

    func testFailureAgeMessage() {
        let e = expectation(description: "testFailureAgeMessage")
        viewController?.buildAgeMessage(for: "xyz") { string in
            XCTAssertEqual(string, "Error retrieving age.")
            e.fulfill()
        }
        waitForExpectations(timeout: 1)
    }
}

i was reading about mocking singleton is tough

这个概念是,如果您将这些 APIManager.shared 引用散布在您的代码中,就很难用模拟对象将它们换掉。注入(inject)解决了这个问题。

然后,再一次,如果您现在已在任何地方注入(inject)此 APIManager 实例以促进模拟并消除所有这些 shared 引用,它会回避您想要的问题为了避免,即为什么不再使用单例?

关于ios - 关于使用 xctest 在 swift 和 ios 中模拟单例的查询?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59378213/

相关文章:

arrays - 如何通过 JSON 将数组从 php 解压到 Swift 中

ios - 无法更改以编程方式创建的 UILabel 中的文本大小

unit-testing - Rhino 模拟 stub 异步方法

google-app-engine - 如何对 Google App Engine Go HTTP 处理程序进行单元测试?

ios - avcodec_receive_packet() 看不到输出

ios - 在 iOS 6.1 上指令 "svc 128"后接收信号 SIGTRAP

objective-c - 将 UIImageView 置于 UIView 之上,需要说明

swift - 为什么迭代闭包会导致 swift 出现总线错误?

java - 测试(模拟)一个 void 函数,该函数在内部调用其他与数据库创建连接的函数

iphone - 在摇动手势上做 IBAction