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

时间:2015-10-07 22:19:37

标签: unit-testing protocols swift2 gamekit

Swift 2中新的面向协议的功能在WWDC上引入了大量的ballyhoo,包括声称“它还使我们不必一直制作模拟”。

这听起来很棒 - 我希望能够在没有嘲笑的情况下编写测试。

所以我为GKMatch建立了一个很好的协议/扩展对,如下所示:

createScaledBitmap(...)

由于GKMatch无法直接实例化,为了在之前版本的Swift中进行测试,我必须构建一个模拟GKMatchmaker,它将返回一个模拟GKMatch,这是一个非常复杂的事情。这就是为什么我的耳朵在WWDC谈话中在那条线上振作起来。

但是如果面向协议的方法能够在这里实现免于模拟,那我就没有看到它。

任何人都可以告诉我如何在不进行模拟的情况下测试此代码吗?

3 个答案:

答案 0 :(得分:3)

我想我已经想到了这一点,感谢GoZoner的运行。这是不同的,我认为它需要一个单独的答案。

首先:WWDC谈话可能指的是测试从头开始使用面向协议的概念构建的东西。在这种情况下,也许可以完全避免嘲弄。

但是:当使用面向对象方法构建的类(例如GKMatch)时,面向协议的概念不会让你避免使用模拟。但是,它们将非常容易地创建模拟

所以:这是一种制作GKMatch的面向协议的模拟方法。

首先使用您要测试的GKMatch方法和属性定义协议:

public protocol GKMatchIsh {
  var players: [GKPlayer] {get}
  func sendData(data: NSData, toPlayers players: [GKPlayer],
  dataMode mode: GKMatchSendDataMode) throws
}

然后声明GKMatch采用该协议

extension GKMatch: GKMatchIsh {}

这就是神奇发生的地方;协议使得模拟非常非常容易。

一旦宣布采用该协议,如果GKMatch 尚未符合协议,您将看到错误。换句话说,您可以绝对肯定您的协议完全与GKMatch中的方法匹配,因为如果没有,extension GKMatch: GKMatchIsh {}将导致错误。所有你需要做的就是纠正GKMatchIsh,直到你没有看到错误,并且你知道你有一个适当模拟的气密定义。

因此,使用该定义,这是一个正确模拟的例子。

注意:我在Playground中输入所有这些,这样我就可以进行非常简单的测试。您可以将所有这些代码粘贴到Playground中以查看它是否运行。但是,如果您熟悉这些概念,那么应该明白如何将概念转移到XCTest框架。

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)!)
  }
}

现在,为了解决测试问题,您可以继续使用面向协议的概念向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)")
    }
  }
}

现在再次指出这里的魔力:因为extension 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)
  }
}

最后,使用Playground,实例化结构并测试它:

let ezMock = EZMatchMock()
let test = WorksWithActualGKMatchToo(match: ezMock)
test.sendArbitraryData()

如果将所有这些粘贴到Playground中,当它运行时,您将在调试控制台中看到“test succeeded”打印出来。虽然您只是直接测试EZMatchMock,但您在技术上正在测试GKMatch 本身

总结一下:如果我做对了,这就是面向协议的概念让你轻松创建非常可靠的模拟,然后轻松扩展他们的行为,然后轻松测试这些扩展 - 知道完全相同的代码将被模拟的真实对象使用。

下面我将上面的所有代码收集到一个块中,这样你就可以将它粘贴到Playground中并看到它有效:

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()

答案 1 :(得分:0)

也许沿着这些方向:

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")

答案 2 :(得分:0)

魔法子弹&#39;大多数解决方案似乎缺少的是能够在运行时存根对象的行为,但很容易依赖于原始行为。我写了一个名为MockFive的小工具,我认为可以解决这个问题。成本是一个小样板,但作为回报,你可以获得OCMock或Cedar的强大功能,但完全可以实现Swifty类型。

只要你有存根,你可以编写一个模拟然后在任何地方使用它,无论你的测试要求是什么 - 只需要你需要的行为。这在类和协议中都有好处:在类中,您的模拟子类可以默认调用super,并且除非您更改它,否则会为您提供vanilla对象行为。在协议中,您创建一个具有合理默认值的模拟,您可以在任何地方替换它。

我在我的博客here上写了一篇关于如何使用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 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"))
            }
        }
    }
}