首页 > 解决方案 > 如何在 iOS 中使用委托模拟外部框架类?

问题描述

我在一个名为的 iOS 应用程序中工作ConnectApp,我正在使用一个名为Connector. 现在,Connector框架完成了与 BLE 设备的实际连接任务,并让我的调用方应用程序(即ConnectApp)通过ConnectionDelegate. 让我们看看示例代码,

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使用 a模拟MockConnector可能会覆盖较低的任务并从MockConnector类返回状态。但是当我需要处理一个ConnectionDelegate返回复杂对象的时候。我该如何解决这个问题?

请注意,框架不提供类的接口,而是我们需要为具体对象(如,等)找到Connector方法ConnectionDelegate

更新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”只能用作通用约束,因为它具有 Self 或关联的类型要求

我究竟做错了什么?

标签: iosoopinheritancedesign-patternsmocking

解决方案


在 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,但让测试代码替代协议的不同实现。由于在调用ConnectionService时会创建一个新实例connect(),因此我们可以将闭包用作简单的工厂方法。生产代码可以提供一个默认闭包(创建一个Connector),就像一个闭包属性:

private let makeConnector: () -> ConnectorProtocol

通过将参数传递给初始化程序来设置其值。初始化器可以指定一个默认值,这样Connector除非另有说明,否则它会变为实数:

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

connect()中,调用makeConnector()而不是Connector()。由于我们没有针对此更改进行单元测试,因此请进行手动测试以确认我们没有破坏任何内容。

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

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

让我们制作一个模拟对象来检查第一部分。setDelegate(delegate:)在调用之前调用很重要connect(),所以让我们在一个数组中模拟记录所有调用。该数组为我们提供了一种检查调用顺序的方法。与其让测试代码检查调用数组(充当仅记录内容的测试间谍),如果我们将其设为成熟的 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调用委托。你知道它确实如此,而且它不在你的控制范围内。相反,编写一个测试来直接调用委托方法。(您仍然希望它发出 aMockConnector以防止对 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
}

推荐阅读