[英]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.