ios - 如何在 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 或关联的类型要求
我究竟做错了什么?
解决方案
在 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
正确吗? - 调用委托方法时会发生什么?
让我们制作一个模拟对象来检查第一部分。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)
}
}
(该业务与传递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
调用委托。你知道它确实如此,而且它不在你的控制范围内。相反,编写一个测试来直接调用委托方法。(您仍然希望它发出 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
}
推荐阅读
- java - 带有可迭代的Spring数据findAllBy返回空数组
- asp.net-mvc - 在 asp.net mvc 中重命名模型
- javascript - 将 jwt 添加到标头时出现 JSON.parse 错误
- python-3.x - python3 pygeocoder 模块以下无法执行,因为它导致错误 OVER_QUERY_LIMIT
- java - 爪哇地图
如何对值求和 - url-redirection - 将亚马逊购买的域重定向到现有 URL
- json - 如何显示来自 Alamofire 请求的 JSON 数组响应的某些部分
- python - 使用 pytest 进行延迟参数化
- alamofire - 使用 Alamofire 5 和 responseDecodable 函数解码错误响应
- python - django.db.utils.IntegrityError: FOREIGN KEY 约束在通过 Selenium 和 Python Django 执行 LiveServerTestCases 时失败