【问题标题】:Mocking in Swift在 Swift 中模拟
【发布时间】:2014-08-02 04:07:51
【问题描述】:

如何在 Swift 中模拟对象?

Mirror 协议听起来很有希望,但它现在并没有多大作用。

到目前为止,我发现的唯一方法是子类化并覆盖模拟类的所有方法。这当然不是真正的模拟,远非理想,而且工作量很大。

还有其他想法吗?

为什么不 OCMock?

来自source

我可以通过语言桥功能使用 OCMock 吗?

是的,但是 有限制。如果你很勇敢。到目前为止,这是高度 实验性的。不能保证 OCMock 将完全支持 斯威夫特。

已知限制:

  • 测试必须用 Objective-C 编写
  • 应该模拟的对象必须从 NSObject 继承
  • 没有存根/期望/验证类方法

【问题讨论】:

  • 我仍然不知道反射在 Swift 中是如何工作的。也许我们需要等到 Apple 发布更多文档和更多 Swift 功能。 (而且 Swift 比 ObjC 更静态,由于静态调度、@final 方法等,这会使模拟变得更加困难)
  • 为什么不使用 OCMock?只需在 Bridging Header 文件中导入框架即可。我就是这么做的。
  • @AlexReynolds 我喜欢 OCMock,但(可以理解)它对 Swift 有严重的限制。我用更多信息更新了这个问题。
  • 如果您也使用 OBJc,则没有真正的限制。我在 swift xctest 中使用 ocmock。您可以通过我在此处的回答stackoverflow.com/questions/24049735/… 解决我知道的唯一限制,如果使用 XCTAssertNoThrow 进行模拟验证
  • @AlexReynolds OCMock 是为 ObjC 设计的。它不适用于斯威夫特。我们现在像 ObjC 代码一样编写 Swift 代码,因为我们曾经使用过 ObjC,但将来不会这样。我们将使用大量 Swift 结构/枚举,而 OCMock 无法处理它们。唯一的解决方案是为 Swift 实现一些 Swift 模拟库。但是我们还不知道如何在 Swift 中模拟非 ObjC 相关的代码。

标签: unit-testing mocking swift


【解决方案1】:

NSHipster 谈到了 Swift 中的语言特性,这些特性使得外部模拟库变得不那么必要:

在 Swift 中,可以在函数定义中声明类,从而允许模拟对象非常独立。只需声明一个模拟内部类、覆盖和 [原文如此] 必要的方法:

func testFetchRequestWithMockedManagedObjectContext() {
    class MockNSManagedObjectContext: NSManagedObjectContext {
        override func executeFetchRequest(request: NSFetchRequest!, error: AutoreleasingUnsafePointer<NSError?>) -> [AnyObject]! {
            return [["name": "Johnny Appleseed", "email": "johnny@apple.com"]]
        }
    }

    ...
}

在本地范围内创建外部依赖项的子类的能力加上 XCTestExpectation 的添加解决了许多与 OCMock 相同的问题。

诸如OCMock 之类的库提供的非常有用的一件事是它的“验证”方法,以确保调用了模拟类。可以手动添加,但自动添加很好。

【讨论】:

  • 这种方法目前不起作用,我的 Xcode 因“SourceKitService Crashed”而崩溃。
  • @Rodrigo 在 6.1.1 中修复了吗?
  • @hris.to 我认为单元测试不应该访问私有方法。理想情况下,您将使用 Inversion of Control 来注入模拟对象。由于您的代码只会访问该模拟对象上的公共方法,因此私有方法是无关紧要的。 stackoverflow.com/questions/3058/what-is-inversion-of-control(编辑:太快按回车。想添加指向 IoC 信息的链接)
  • 从理论上讲你是对的。但是,在某些情况下,您需要访问私有变量(或方法)。例如,如果您有一个基于输入设置内部(私有)标志的公共方法。如果您无权访问此标志来检查其状态,则您无法根据输入知道该方法是否正常工作。
  • @hris.to 我非常不同意公开内部功能 ;-) 您正在测试的类旨在作为更大系统的一部分工作,因此它所做的一切都会产生某种影响其他组件可以观察到 - 因此可以通过测试以某种方式观察到。 (如果你所做的事情不能被外部观察到,那你为什么要写代码?!)
【解决方案2】:

我通过将所有内容包装在一个协议中来创建我的模拟类。我手动滚动一个模拟类以符合相关协议,如下所示:

protocol Dog: class {
    var name: String { get }

    func bark()
}

class DogImpl: Dog {
    var name: String

    init(name: String) {
        self.name = name
    }

    func bark() {
        print("Bark!")
    }
}

class DogMock: Dog {
    var name = "Mock Dog"
    var didBark = false

    func bark() {
        didBark = true
    }
}

我将它与依赖注入结合使用以实现完整的测试覆盖率。这是很多样板文件,但到目前为止我对这种方法没有任何问题。

关于子类模拟,你会遇到final 类的麻烦,或者如果它们有非平凡的初始化程序。

【讨论】:

  • 还有一些库可以根据您的协议自动生成模拟!
【解决方案3】:

除了标记的答案之外,我还想指出一些东西 - 我不知道这是否是一个错误。

如果您以某种方式子类化 NSObject(在我的情况下,我是子类化 UIView,内部子类化 NSObject)您需要使用 @objc 显式声明覆盖的函数,否则您的测试将无法编译。在我的例子中,编译器本身崩溃了:

分段错误:11

所以下面的类:

public class ClassA: UIView
{
    @objc public func johnAppleseed() {

    }
}

应该通过以下方式进行单元测试:

class ClassATests: XCTestCase {

    func testExample()
    {
        class ClassAChildren: ClassA
        {
            @objc private override func johnAppleseed() {

            }
        }
    }
}

【讨论】:

    【解决方案4】:

    我推荐使用Cuckoo,它可以处理大多数标准的模拟任务。

    示例类:

    class ExampleObject {
    
        var number: Int = 0
    
        func evaluate(number: Int) -> Bool {
            return self.number == number
        }
    
    }
    
    class ExampleChecker {
    
        func check(object: ExampleObject) -> Bool {
            return object.evaluate(5)
        }
    
    }
    

    示例测试:

    @testable import App
    import Cuckoo
    import XCTest
    
    class ExampleCheckerTests: XCTestCase {
    
        func testCheck() {
            // 1. Arrange
            let object = MockExampleObject().spy(on: ExampleObject())
            stub(object) { object in
                when(object.evaluate(any())).thenDoNothing()
            }
            let checker = ExampleChecker()
    
            // 2. Action
            checker.check(object)
    
            // 3. Assert
            _ = verify(object).number.get
            verify(object).evaluate(any())
            verifyNoMoreInteractions(object)
        }
    
    }
    

    【讨论】:

    • 相当整洁。系统库怎么样?它可以在NetworkExtension 中模拟NEVPNConnection 吗?
    【解决方案5】:

    您可以使用MockFive 实现这种模拟。

    它的要点是您必须“手动”创建要模拟的源类的模拟-

    但是,您可以动态地存根它的方法,因此您可以在任何需要的地方使用它并在那里配置它的行为。

    我写了一篇关于如何使用它的文章here

    【讨论】:

      【解决方案6】:

      由于您编写的限制,OCMock 在 Swift 中不能很好地工作(因为每个模拟框架都强烈依赖于运行时)。

      不过,Swift 有几个模拟框架,从半手动到几乎全自动的模拟生成。其中一些已经列在答案中,所以我只推荐另一个,我是作者之一。

      https://github.com/MakeAWishFoundation/SwiftyMocky

      我不会详细介绍,它有一些小的限制,但据我所知,它具有 Swift 框架中最广泛的特性(至少是我所知道的),包括泛型支持、@objc 成员和在您编写/更改协议时更新模拟(观察者模式)。

      【讨论】:

        猜你喜欢
        • 2016-10-22
        • 1970-01-01
        • 1970-01-01
        • 2016-03-31
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-09-01
        相关资源
        最近更新 更多