[英]How to mock classes of external framework with delegates in iOS?
我在一個名為ConnectApp
的 iOS 應用程序中工作,我正在使用一個名為Connector
的框架。 現在, Connector
框架完成了與 BLE 設備的實際連接任務,並通過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 設備,我們需要模擬框架的連接器層。 在簡單類(即使用更簡單的方法)的情況下,我們可以使用繼承並使用MockConnector
模擬Connector
,該MockConnector
可能會覆蓋較低的任務並從MockConnector
類返回狀態。 但是當我需要處理一個返回復雜對象的ConnectionDelegate
。 我該如何解決這個問題?
請注意,框架不提供類的接口,而我們需要為具體對象(如Connector
、 ConnectionDelegate
等)找到方法。
嘗試應用 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 就位,所以我們可以開始編寫測試了。 有兩種類型的測試要編寫:
Connector
? 讓我們制作一個模擬對象來檢查第一部分。 在調用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)
}
}
(傳遞file
和line
?這使得任何測試失敗都會報告調用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.