温馨提示:本文翻译自stackoverflow.com,查看原文请点击:oop - How to mock classes of external framework with delegates in iOS?
design-patterns inheritance ios mocking oop

oop - 如何在iOS中使用委托模拟外部框架的类?

发布于 2020-04-11 13:06:51

我正在ConnectApp使用一个名为的iOS应用程序,并且正在使用一个名为的框架Connector现在,Connector框架完成了与BLE设备的实际连接任务,并通过通知了我的呼叫者应用(即ConnectAppConnectionDelegate让我们看示例代码,

ConnectApp-主机应用

class ConnectionService: ConnectionDelegate {

    func connect(){
        var connector = Connector()
        connector.setDelegate(self)
        connector.connect()
    }

    func onConnected(result: ConnectionResult) {
        //connection result
    }
}

连接器框架

public class ConnectionResult {
    // many complicated custom variables
}

public protocol ConnectionDelegate {
      func onConnected(result: ConnectionResult)
}

public class Connector {

   var delegate: ConnectionDelegate?

   func setDelegate(delegate: ConnectionDelegate) {
       self.delegate = delegate
   }

   func connect() {
        //…..
        // result = prepared from framework
        delegate?.onConnected(result)
   }
}

问题

有时开发人员没有BLE设备,我们需要模拟框架的Connector层。对于简单的类(例如,使用更简单的方法),我们可以使用继承并使用来模拟ConnectorMockConnector这可能会覆盖较低的任务并从MockConnector类中返回状态但是当我需要处理一个ConnectionDelegate返回复杂对象的对象时。我该如何解决这个问题?

需要注意的是框架不提供的类接口,而我们需要寻找周围的道路像,具体对象ConnectorConnectionDelegate等等。

更新1:

尝试应用Skwiggs的答案,所以我创建了类似的协议,

protocol ConnectorProtocol: Connector {
    associatedType MockResult: ConnectionResult
}

然后使用类似的策略模式注入真实/模拟

class ConnectionService: ConnectionDelegate {

    var connector: ConnectorProtocol? // Getting compiler error
    init(conn: ConnectorProtocol){
        connector = conn
    }

    func connect(){
        connector.setDelegate(self)
        connector.connect()
    }

    func onConnected(result: ConnectionResult) {
        //connection result
    }
}

现在我收到编译器错误,

协议“ ConnectorProtocol”只能用作通用约束,因为它具有“自我”或相关类型要求

我究竟做错了什么?

查看更多

提问者
Sazzad Hissain Khan
被浏览
86
Jon Reid 2020-02-02 03:24

在Swift中,创建Seam(允许我们替换不同的实现的分离)的最简单方法是定义协议。这需要更改生产代码以与协议对话,而不是像那样的硬编码依赖项Connector()

首先,创建协议。Swift让我们将新协议附加到现有类型上。

protocol ConnectorProtocol {}

extension Connector: ConnectorProtocol {}

这定义了一个协议,最初是空的。它说Connector符合该协议。

协议中包含什么?您可以通过改变类型发现这个var connector从隐Connector到显式ConnectorProtocol

var connector: ConnectorProtocol = Connector()

Xcode会抱怨未知的方法。通过将所需的每种方法的签名复制到协议中来使其满意。从您的代码示例来看,可能是:

protocol ConnectorProtocol {
    func setDelegate(delegate: ConnectionDelegate)
    func connect()
}

由于Connector已经实现了这些方法,因此可以满足协议扩展。

接下来,我们需要一种使用生产代码的方法Connector,但是需要一种使用测试代码替代协议的不同实现的方法。由于ConnectionServiceconnect()调用时会创建一个新实例,因此我们可以将闭包用作简单的Factory方法。生产代码可以提供一个默认的闭包(创建一个Connector),例如带有闭包属性:

private let makeConnector: () -> ConnectorProtocol

通过将参数传递给初始化程序来设置其值。初始化程序可以指定一个默认值,以便使它成为实数,Connector除非另有说明:

init(makeConnector: (() -> ConnectorProtocol) = { Connector() }) {
    self.makeConnector = makeConnector
    super.init()
}

输入connect()makeConnector()而不是Connector()由于我们没有针对此更改的单元测试,因此请进行手动测试以确认我们没有破坏任何东西。

现在我们的接缝就位了,所以我们可以开始编写测试了。编写两种类型的测试:

  1. 我们打的Connector正确吗?
  2. 调用委托方法时会发生什么?

让我们制作一个模拟对象来检查第一部分。在调用setDelegate(delegate:)之前先调用很重要connect(),因此让我们模拟记录数组中的所有调用。该数组为我们提供了一种检查呼叫顺序的方法。如果不使测试代码检查调用数组(充当仅记录内容的Test Spy),则如果我们将其作为完整的Mock对象,则您的测试将变得更加干净-这意味着它将进行自己的验证。

final class MockConnector: ConnectorProtocol {
    private enum Methods {
        case setDelegate(ConnectionDelegate)
        case connect
    }

    private var calls: [Methods] = []

    func setDelegate(delegate: ConnectionDelegate) {
        calls.append(.setDelegate(delegate))
    }

    func connect() {
        calls.append(.connect)
    }

    func verifySetDelegateThenConnect(
        expectedDelegate: ConnectionDelegate,
        file: StaticString = #file,
        line: UInt = #line
    ) {
        if calls.count != 2 {
            fail(file: file, line: line)
            return
        }
        guard case let .setDelegate(delegate) = calls[0] else {
            fail(file: file, line: line)
            return
        }
        guard case .connect = calls[1] else {
            fail(file: file, line: line)
            return
        }
        if expectedDelegate !== delegate {
            XCTFail(
                "Expected setDelegate(delegate:) with \(expectedDelegate), but was \(delegate)",
                file: file,
                line: line
            )
        }
    }

    private func fail(file: StaticString, line: UInt) {
        XCTFail("Expected setDelegate(delegate:) followed by connect(), but was \(calls)", file: file, line: line)
    }
}

(该业务带有传递fileline?这使得所有测试失败都将报告调用的行verifySetDelegateThenConnect(expectedDelegate:),而不是报告的行XCTFail(_)。)

这是您如何在中使用此方法ConnectionServiceTests

func test_connect_shouldMakeConnectorSettingSelfAsDelegateThenConnecting() {
    let mockConnector = MockConnector()
    let service = ConnectionService(makeConnector: { mockConnector })

    service.connect()

    mockConnector.verifySetDelegateThenConnect(expectedDelegate: service)
}

这样就可以处理第一种类型的测试。对于第二种类型,无需测试Connector调用委托。您知道的确如此,这是您无法控制的。而是编写一个测试以直接调用委托方法。(您仍然希望它制作一个,MockConnector以防止对real的任何调用Connector)。

func test_onConnected_withCertainResult_shouldDoSomething() {
    let service = ConnectionService(makeConnector: { MockConnector() })
    let result = ConnectionResult(…) // Whatever you need

    service.onConnected(result: result)

    // Whatever you want to verify
}

这些技术在iOS单元测试中通过示例 进行了更详细的描述:XCTest使用Swift的技巧和技巧https://pragprog.com/book/jrlegios/ios-unit-testing-by-example