简体   繁体   English

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

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

I am working in an iOS application called ConnectApp and I am using a framework called Connector .我在一个名为ConnectApp的 iOS 应用程序中工作,我正在使用一个名为Connector的框架。 Now, Connector framework completes actual connection task with BLE devices and let my caller app (ie ConnectApp ) know the connection request results through ConnectionDelegate .现在, Connector框架完成了与 BLE 设备的实际连接任务,并通过ConnectionDelegate让我的调用方应用程序(即ConnectApp )知道连接请求结果。 Let's see example code,让我们看看示例代码,

ConnectApp - host app ConnectApp - 主机应用程序

class ConnectionService: ConnectionDelegate {

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

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

Connector Framework连接器框架

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)
   }
}

Problem问题

Sometimes developers have no BLE device and we need to mock the Connector layer of framework.有时开发人员没有 BLE 设备,我们需要模拟框架的连接器层。 In case of simple classes (ie with simpler methods) we could have used inheritance and mock the Connector with a MockConnector which might override the lower tasks and return status from MockConnector class.在简单类(即使用更简单的方法)的情况下,我们可以使用继承并使用MockConnector模拟Connector ,该MockConnector可能会覆盖较低的任务并从MockConnector类返回状态。 But when I need to deal with a ConnectionDelegate which returns complicated object.但是当我需要处理一个返回复杂对象的ConnectionDelegate How can I resolve this issue?我该如何解决这个问题?

Note that framework does not provide interfaces of the classes rather we need to find way around for concrete objects like, Connector , ConnectionDelegate etc.请注意,框架不提供类的接口,而我们需要为具体对象(如ConnectorConnectionDelegate等)找到方法。

Update 1:更新 1:

Trying to apply Skwiggs's answer so I created protocol like,尝试应用 Skwiggs 的答案,所以我创建了协议,例如,

protocol ConnectorProtocol: Connector {
    associatedType MockResult: ConnectionResult
}

And then injecting real/mock using strategy pattern like,然后使用策略模式注入真实/模拟,例如,

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
    }
}

Now I am getting compiler error,现在我收到编译器错误,

Protocol 'ConnectorProtocol' can only be used as a generic constraint because it has Self or associated type requirements协议 'ConnectorProtocol' 只能用作通用约束,因为它具有 Self 或关联的类型要求

What am I doing wrong?我究竟做错了什么?

In Swift, the cleanest way to create a Seam (a separation that allows us to substitute different implementations) is to define a protocol.在 Swift 中,创建 Seam(一种允许我们替换不同实现的分离)的最简洁方法是定义一个协议。 This requires changing the production code to talk to the protocol, instead of a hard-coded dependency like Connector() .这需要更改生产代码以与协议对话,而不是像Connector()这样的硬编码依赖项。

First, create the protocol.首先,创建协议。 Swift lets us attach new protocols to existing types. Swift 允许我们将新协议附加到现有类型。

protocol ConnectorProtocol {}

extension Connector: ConnectorProtocol {}

This defines a protocol, initially empty.这定义了一个协议,最初是空的。 And it says that Connector conforms to this protocol.它说Connector符合这个协议。

What belongs in the protocol?什么属于协议? You can discover this by changing the type of var connector from the implicit Connector to an explicit ConnectorProtocol :您可以通过改变类型发现这个var connector从隐含的Connector ,以明确ConnectorProtocol

var connector: ConnectorProtocol = Connector()

Xcode will complain about unknown methods. Xcode 会抱怨未知的方法。 Satisfy it by copying the signature of each method it needs into the protocol.通过将它需要的每个方法的签名复制到协议中来满足它。 Judging from your code sample, it may be:从你的代码示例来看,它可能是:

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

Because Connector already implements these methods, the protocol extension is satisfied.因为Connector已经实现了这些方法,所以满足了协议扩展。

Next, we need a way for the production code to use Connector , but for test code to substitute a different implementation of the protocol.接下来,我们需要一种让生产代码使用Connector ,但让测试代码替换协议的不同实现。 Since ConnectionService creates a new instance when connect() is called, we can use a closure as a simple Factory Method.由于ConnectionService在调用connect()时会创建一个新实例,因此我们可以使用闭包作为简单的工厂方法。 The production code can supply a default closure (creating a Connector ) like with a closure property:生产代码可以提供一个默认的闭包(创建一个Connector ),就像一个闭包属性:

private let makeConnector: () -> ConnectorProtocol

Set its value by passing an argument to the initializer.通过将参数传递给初始值设定项来设置其值。 The initializer can specify a default value, so that it makes a real Connector unless told otherwise:初始值设定项可以指定一个默认值,以便它生成一个真正的Connector除非另有说明:

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

In connect() , call makeConnector() instead of Connector() .connect() ,调用makeConnector()而不是Connector() Since we don't have unit tests for this change, do a manual test to confirm we didn't break anything.由于我们没有针对此更改进行单元测试,因此请进行手动测试以确认我们没有破坏任何内容。

Now our Seam is in place, so we can begin writing tests.现在我们的 Seam 就位,所以我们可以开始编写测试了。 There are two types of tests to write:有两种类型的测试要编写:

  1. Are we calling Connector correctly?我们是否正确调用了Connector
  2. What happens when the delegate method is called?调用委托方法时会发生什么?

Let's make a Mock Object to check the first part.让我们制作一个模拟对象来检查第一部分。 It's important that we call setDelegate(delegate:) before calling connect() , so let's have the mock record all calls in an array.在调用connect()之前调用setDelegate(delegate:)很重要,所以让模拟记录数组中的所有调用。 The array gives us a way to check the call order.该数组为我们提供了一种检查调用顺序的方法。 Instead of having the test code examine the array of calls (acting as a Test Spy which just records stuff), your test will be cleaner if we make this a full-fledged Mock Object — meaning it will do its own verification.与其让测试代码检查调用数组(充当只记录内容的测试间谍),如果我们将其设为成熟的 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)
    }
}

(That business with passing around file and line ? This makes it so that any test failure will report the line that calls verifySetDelegateThenConnect(expectedDelegate:) , instead of the line that calls XCTFail(_) .) (传递fileline ?这使得任何测试失败都会报告调用verifySetDelegateThenConnect(expectedDelegate:)的行,而不是调用XCTFail(_) 。)

Here's how you'd use this in ConnectionServiceTests :以下是在ConnectionServiceTests使用它的方法:

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

    service.connect()

    mockConnector.verifySetDelegateThenConnect(expectedDelegate: service)
}

That takes care of the first type of test.这将处理第一种类型的测试。 For the second type, there's no need to test that Connector calls the delegate.对于第二种类型,不需要测试Connector调用了委托。 You know it does, and it's outside your control.你知道它确实存在,而且它不在你的控制范围内。 Instead, write a test to call the delegate method directly.相反,编写一个测试来直接调用委托方法。 (You'll still want it to make a MockConnector to prevent any calls to the real 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
}

You could try你可以试试

protocol MockConnector: Connector {
    associatedType MockResult: ConnectionResult
}

Then, for each connector you need to mock, define a concrete class that conforms to this mock connector然后,对于每个需要模拟的连接器,定义一个符合这个模拟连接器的具体类

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