Swift 2中新的面向协议的功能在WWDC上引入了大量的ballyhoo,包括声称“它还使我们不必一直制作模拟”。
这听起来很棒 - 我希望能够在没有嘲笑的情况下编写测试。
所以我为GKMatch建立了一个很好的协议/扩展对,如下所示:
createScaledBitmap(...)
由于GKMatch无法直接实例化,为了在之前版本的Swift中进行测试,我必须构建一个模拟GKMatchmaker,它将返回一个模拟GKMatch,这是一个非常复杂的事情。这就是为什么我的耳朵在WWDC谈话中在那条线上振作起来。
但是如果面向协议的方法能够在这里实现免于模拟,那我就没有看到它。
任何人都可以告诉我如何在不进行模拟的情况下测试此代码吗?
答案 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"))
}
}
}
}