这不是关于我们是否应该使用单例的问题。而是 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/