我正在ConnectApp
使用一个名为的iOS应用程序,并且正在使用一个名为的框架Connector
。现在,Connector
框架完成了与BLE设备的实际连接任务,并通过通知了我的呼叫者应用(即ConnectApp
)ConnectionDelegate
。让我们看示例代码,
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层。对于简单的类(例如,使用更简单的方法),我们可以使用继承并使用来模拟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”只能用作通用约束,因为它具有“自我”或相关类型要求
我究竟做错了什么?
在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()
调用时会创建一个新实例,因此我们可以将闭包用作简单的Factory方法。生产代码可以提供一个默认的闭包(创建一个Connector
),例如带有闭包属性:
private let makeConnector: () -> ConnectorProtocol
通过将参数传递给初始化程序来设置其值。初始化程序可以指定一个默认值,以便使它成为实数,Connector
除非另有说明:
init(makeConnector: (() -> ConnectorProtocol) = { Connector() }) {
self.makeConnector = makeConnector
super.init()
}
输入connect()
,makeConnector()
而不是Connector()
。由于我们没有针对此更改的单元测试,因此请进行手动测试以确认我们没有破坏任何东西。
现在我们的接缝就位了,所以我们可以开始编写测试了。编写两种类型的测试:
Connector
正确吗?让我们制作一个模拟对象来检查第一部分。在调用setDelegate(delegate:)
之前先调用很重要connect()
,因此让我们模拟记录数组中的所有调用。该数组为我们提供了一种检查呼叫顺序的方法。如果不使测试代码检查调用数组(充当仅记录内容的Test Spy),则如果我们将其作为完整的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
以防止对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
}
这些技术在iOS单元测试中通过示例 进行了更详细的描述:XCTest使用Swift的技巧和技巧https://pragprog.com/book/jrlegios/ios-unit-testing-by-example