Unit testing Swift 2:使用面向协议的编程而不是模拟对象进行测试?

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

Swift 2中新的面向协议的功能在WWDC上被大肆宣传,包括“它还使我们不必一直进行模拟”的声明

听起来很棒——我希望能够在没有模拟的情况下编写测试

因此,我为GKMatch设置了一个很好的协议/扩展对,如下所示:

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