Unit testing Swift 2:使用面向协议的编程而不是模拟对象进行测试?
Swift 2中新的面向协议的功能在WWDC上被大肆宣传,包括“它还使我们不必一直进行模拟”的声明 听起来很棒——我希望能够在没有模拟的情况下编写测试 因此,我为GKMatch设置了一个很好的协议/扩展对,如下所示:Unit testing Swift 2:使用面向协议的编程而不是模拟对象进行测试?,unit-testing,protocols,swift2,gamekit,Unit Testing,Protocols,Swift2,Gamekit,Swift 2中新的面向协议的功能在WWDC上被大肆宣传,包括“它还使我们不必一直进行模拟”的声明 听起来很棒——我希望能够在没有模拟的情况下编写测试 因此,我为GKMatch设置了一个很好的协议/扩展对,如下所示: protocol SendData { func send(data: NSData) } extension GKMatch: SendData { func send(data: NSData) { do { try self.sendData(d
protocol SendData {
func send(data: NSData)
}
extension GKMatch: SendData {
func send(data: NSData) {
do {
try self.sendData(data, toPlayers: self.players,
dataMode: .Reliable)
} catch {
print("sendData failed with message: \(error)")
}
}
}
//Now what? How to test without a GKMatch or mock GKMatch?
由于GKMatch不能直接实例化,为了在以前版本的Swift中进行测试,我必须构建一个模拟GKMatchmaker,它将返回一个模拟GKMatch,这是一件非常复杂的事情。这就是为什么我在WWDC演讲中听到这句话时,耳朵都竖了起来
但是,如果面向协议的方法在这里实现了从模拟中解放出来的自由,我看不到它
有人能告诉我如何在不进行模拟的情况下测试这段代码吗 可能是这样的:
protocol SendData {
func send (data: NSData)
}
protocol HasSendDataToPlayers {
func sendData(_ data: NSData,
toPlayers players: [GKPlayer],
dataMode mode: GKMatchSendDataMode) throws
}
extension SendData where Self == HasSendDataToPlayers { // 'where' might be off
func send(data: NSData) {
do {
try self.sendData(data, toPlayers: self.players,
dataMode: .Reliable)
} catch {
print("sendData failed with message: \(error)")
}
}
}
// Test Support (did I 'move the mock'?)
struct MyMatch : HasSendDataToPlayers {
func sendData(_ data: NSData,
toPlayers players: [GKPlayer],
dataMode mode: GKMatchSendDataMode) throws {
print("Got data")
}
}
XCTAssertEquals(MyMatch().send(<data>), "Got data")
协议发送数据{
func发送(数据:NSData)
}
协议HasSendDataToPlayers{
func sendData(data:NSData,
顶级球员:[GKPlayer],
dataMode模式:GKMatchSendDataMode)抛出
}
扩展SendData,其中Self==HasSendDataToPlayers{//'where'可能处于禁用状态
func发送(数据:NSData){
做{
试试self.sendData(数据,顶层:self.players,
数据模式:。可靠)
}抓住{
打印(“发送数据失败,消息:\(错误)”)
}
}
}
//测试支持(我是否‘移动模拟’?)
结构MyMatch:HasSendDataToPlayers{
func sendData(data:NSData,
顶级球员:[GKPlayer],
dataMode模式:GKMatchSendDataMode)抛出{
打印(“获取数据”)
}
}
xctasertequals(MyMatch().send(),“获取数据”)
多亏了GoZoner的努力,我想我已经明白了这一点。这已经足够不同了,我认为应该有一个单独的答案
第一:WWDC会谈可能指的是测试从一开始就使用面向协议的概念构建的东西。在这种情况下,也许可以完全避免模拟
但是:当使用使用面向对象方法构建的类时,例如GKMatch
,面向协议的概念不会让您避免模拟。但是,它们将使模拟的创建非常、非常容易
所以:这里有一种方法可以制作一个面向协议的GKMatch模拟
首先,使用要测试的GKMatch方法和属性定义协议:
public protocol GKMatchIsh {
var players: [GKPlayer] {get}
func sendData(data: NSData, toPlayers players: [GKPlayer],
dataMode mode: GKMatchSendDataMode) throws
}
然后声明GKMatch采用该协议:
这就是魔法发生的地方;协议使得模仿非常非常容易
一旦声明采用该协议,如果GKMatch不符合该协议,您将看到一个错误。换句话说,您可以完全确定您的协议与GKMatch中的方法完全匹配,因为如果它不匹配,扩展GKMatch:GKMatchIsh{}
将导致错误。您所要做的就是更正GKMatchIsh
,直到您没有看到错误,并且您知道您已经获得了一个正确模拟的气密定义
因此,使用这个定义,这里有一个适当的模拟示例
注意:我在一个操场上输入所有这些,这样我可以做非常简单的测试。您可以将所有这些代码粘贴到游乐场中以查看其运行。但是,如果您熟悉XCTest框架,那么如何将这些概念转移到XCTest框架应该是显而易见的
现在,让我们回到测试问题上来,这里有一种方法,您可以继续使用面向协议的概念将行为添加到GKMatch
并对其进行测试。您不需要我最初尝试的SendData
协议。您可以直接扩展GKMatchIsh
:
extension GKMatchIsh {
public func send(data: NSData) {
do {
try self.sendData(data, toPlayers: self.players, dataMode: .Reliable)
} catch {
print("sendData failed with message: \(error)")
}
}
}
现在再次指出这里的神奇之处:由于扩展名GKMatch:GKMatchIsh{},我们知道这将实际适用于GKMatch,因为如果它不这样做,它将抛出一个错误。你对你的模拟所做的任何测试都应该是对GKMatch的有效测试
这里有一种方法可以测试GKMatchIsh
。创建一个接受GKMatchIsh
对象的结构,并使用它调用刚刚定义的新方法:
public struct WorksWithActualGKMatchToo {
var match: GKMatchIsh
var testData = "test succeeded".dataUsingEncoding(NSUTF8StringEncoding)!
public init (match: GKMatchIsh) {
self.match = match
}
public func sendArbitraryData() {
match.send(testData)
}
}
最后,使用游乐场,实例化结构并测试它:
let ezMock = EZMatchMock()
let test = WorksWithActualGKMatchToo(match: ezMock)
test.sendArbitraryData()
如果您将所有这些粘贴到一个游乐场中,当它运行时,您将在调试控制台中看到“测试成功”打印出来。尽管您只是直接测试EZMatchMock
,但您在技术上也在测试GKMatch
本身
总而言之:如果我没弄错的话,这就是面向协议的概念如何让您轻松创建非常可靠的模拟,然后轻松扩展它们的行为,然后轻松测试这些扩展——知道被模拟的真实对象将使用完全相同的代码
下面,我已将上面的所有代码收集到一个块中,因此您可以将其粘贴到操场中,并查看其工作情况:
import Foundation
import GameKit
public protocol GKMatchIsh {
var players: [GKPlayer] {get}
func sendData(data: NSData, toPlayers players: [GKPlayer],
dataMode mode: GKMatchSendDataMode) throws
}
extension GKMatch: GKMatchIsh {}
public struct EZMatchMock: GKMatchIsh {
public var players = [GKPlayer.anonymousGuestPlayerWithIdentifier("fakePlayer")]
public init() {}
public func sendData(data: NSData, toPlayers players: [GKPlayer],
dataMode mode: GKMatchSendDataMode) throws {
//This is where you put the code for a successful test result.
//You could, for example, set a variable that you'd check afterward with
//an XCTAssert statement.
//Here, we're just printing out the data that's passed in, and when we run
//it we'll see in the Playground console if it prints properly.
print(String(data: data, encoding: NSUTF8StringEncoding)!)
}
}
extension GKMatchIsh {
public func send(data: NSData) {
do {
try self.sendData(data, toPlayers: self.players, dataMode: .Reliable)
} catch {
print("sendData failed with message: \(error)")
}
}
}
public struct WorksWithActualGKMatchToo {
var match: GKMatchIsh
var testData = "test succeeded".dataUsingEncoding(NSUTF8StringEncoding)!
public init (match: GKMatchIsh) {
self.match = match
}
public func sendArbitraryData() {
match.send(testData)
}
}
let ezMock = EZMatchMock()
let test = WorksWithActualGKMatchToo(match: ezMock)
test.sendArbitraryData()
大多数解决方案似乎缺少的“魔弹”是在运行时存根对象行为的能力,但很容易依赖于原始行为。我写了一个叫做的小工具,我认为它解决了这个问题。成本是一个小样板,但作为回报,你得到了奥克莫克或雪松的力量,但完全快速型安全 只要您有存根,您就可以编写一个模拟,然后在任何地方使用它,不管您的测试需求是什么——只要存根到您需要的行为中即可。这在类和协议中都有好处:在类中,您的模拟子类可以在默认情况下调用super,并提供普通的对象行为,除非您更改它。在协议中,您创建一个具有合理默认值的模拟,并且可以在任何地方替换它 我在我的博客上写了一篇关于如何使用MockFive实现这一点的文章,但其要点是 像这样的课
class SimpleDataSource: NSObject, UITableViewDataSource {
@objc func numberOfSectionsInTableView(tableView: UITableView) -> Int { return 1 }
@objc func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 4 }
@objc func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel?.text = "Production Text"
return cell
}
}
class SimpleDataSourceMock: SimpleDataSource, Mock {
let mockFiveLock = lock()
@objc override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return stub(identifier: "number of sections", arguments: tableView) { _ in
super.numberOfSectionsInTableView(tableView)
}
}
@objc override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return stub(identifier: "number of rows in section", arguments: tableView, section) { _ in
super.tableView(tableView, numberOfRowsInSection: section)
}
}
@objc override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
return stub(identifier: "cell for row", arguments: tableView, indexPath) { _ in
super.tableView(tableView, cellForRowAtIndexPath: indexPath)
}
}
}
你写了一个这样的模拟
class SimpleDataSource: NSObject, UITableViewDataSource {
@objc func numberOfSectionsInTableView(tableView: UITableView) -> Int { return 1 }
@objc func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 4 }
@objc func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel?.text = "Production Text"
return cell
}
}
class SimpleDataSourceMock: SimpleDataSource, Mock {
let mockFiveLock = lock()
@objc override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return stub(identifier: "number of sections", arguments: tableView) { _ in
super.numberOfSectionsInTableView(tableView)
}
}
@objc override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return stub(identifier: "number of rows in section", arguments: tableView, section) { _ in
super.tableView(tableView, numberOfRowsInSection: section)
}
}
@objc override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
return stub(identifier: "cell for row", arguments: tableView, indexPath) { _ in
super.tableView(tableView, cellForRowAtIndexPath: indexPath)
}
}
}
并得到这样的行为
class ViewControllerSpecs: QuickSpec {
override func spec() {
let mockDataSource = SimpleDataSourceMock()
let controller = ViewController(dataSource: mockDataSource)
beforeEach {
mockDataSource.resetMock()
}
describe("default behavior of SimpleDataSource") {
beforeEach {
controller.tableView.reloadData()
}
it("should have the correct number of rows") {
expect(controller.tableView.visibleCells.count).to(equal(4))
}
it("should put the correct text on the cells") {
expect(controller.tableView.visibleCells.first?.textLabel?.text).to(equal("Production Text"))
}
it("should interrogate the data source about the number of rows in the first section") {
expect(mockDataSource.invocations).to(contain("tableView(_: tableView, numberOfRowsInSection: 0) -> Int"))
}
}
describe("when I change the behavior of SimpleDataSource") {
beforeEach {
mockDataSource.registerStub("number of sections") { _ in 3 }
mockDataSource.registerStub("number of rows in section") { args -> Int in
let section = args[1]! as! Int
switch section {
case 0: return 2
case 1: return 3
case 2: return 4
default: return 0
}
}
mockDataSource.registerStub("cell for row") { _ -> UITableViewCell in
let cell = UITableViewCell()
cell.textLabel?.text = "stub"
return cell
}
controller.tableView.reloadData()
controller.tableView.layoutIfNeeded()
}
it("should have the correct number of sections") {
expect(controller.tableView.numberOfSections).to(equal(3))
}
it("should have the correct number of rows per section") {
expect(controller.tableView.numberOfRowsInSection(0)).to(equal(2))
expect(controller.tableView.numberOfRowsInSection(1)).to(equal(3))
expect(controller.tableView.numberOfRowsInSection(2)).to(equal(4))
}
it("should interrogate the data source about the number of rows in the first three sections") {
expect(mockDataSource.invocations).to(contain("tableView(_: tableView, numberOfRowsInSection: 0) -> Int"))
expect(mockDataSource.invocations).to(contain("tableView(_: tableView, numberOfRowsInSection: 1) -> Int"))
expect(mockDataSource.invocations).to(contain("tableView(_: tableView, numberOfRowsInSection: 2) -> Int"))
}
it("should have the correct cells") {
expect(controller.tableView.cellForRowAtIndexPath(NSIndexPath(forRow: 0, inSection: 0))!.textLabel!.text).to(equal("stub"))
}
}
}
}
sendData()是一个GKMatch方法,可以访问,因为它位于GKMatch的扩展中。是的,它可能是另一个名称的mock<代码>扩展SendData,其中Self==HasSendDataToPla