繁体   English   中英

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

[英]How to mock classes of external framework with delegates in iOS?

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

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 设备,我们需要模拟框架的连接器层。 在简单类(即使用更简单的方法)的情况下,我们可以使用继承并使用MockConnector模拟Connector ,该MockConnector可能会覆盖较低的任务并从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' 只能用作通用约束,因为它具有 Self 或关联的类型要求

我究竟做错了什么?

在 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. 调用委托方法时会发生什么?

让我们制作一个模拟对象来检查第一部分。 在调用connect()之前调用setDelegate(delegate:)很重要,所以让模拟记录数组中的所有调用。 该数组为我们提供了一种检查调用顺序的方法。 与其让测试代码检查调用数组(充当只记录内容的测试间谍),如果我们将其设为成熟的 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以防止对真正的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
}

你可以试试

protocol MockConnector: Connector {
    associatedType MockResult: ConnectionResult
}

然后,对于每个需要模拟的连接器,定义一个符合这个模拟连接器的具体类

class SomeMockConnector: MockConnector {
    struct MockResult: ConnectionResult {
        // Any mocked variables for this connection result here 
    }

    // implement any further requirements from the Connector class
    var delegate: ConnectionDelegate?

    func connect() {
        // initialise your mock result with any specific data
        let mockResult = MockResult()
        delegate?.onConnected(mockResult)
    }
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM