簡體   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