【问题标题】:Resetting singleton state when testing a module测试模块时重置单例状态
【发布时间】:2018-10-23 19:47:36
【问题描述】:

我在一个 swift 项目中有一个单身人士(是的,我知道人们不喜欢这些,但如果你能暂时忽略这些,我将不胜感激)。

我正在编写一些不测试该单例的单元测试,但他们正在测试的功能取决于该单例的状态。单例是用静态 let 声明的,并且构造函数无论如何都是私有的,因此不能像这样重置它。

如果我正在运行一个单元测试,我可以通过在 setUp() 方法中设置一个从单例读取的变量来正确设置它,但是当我尝试对整个模块运行测试时,它使用第一个调用它的 setUp() 进行设置,然后它不会被重新实例化。所以基本上它卡在整个模块的状态中,这对我来说没有意义 - 我本来希望在测试之间重置所有内容。

有没有办法强制 XCTest 重置测试空间,以确保每次运行新测试文件而不是移动到新模块时重置此单例?

【问题讨论】:

  • 我想你只是跑了,首先,为什么人们不建议使用单例,至少直接。请参阅stackoverflow.com/a/52879968/3141234,了解我对如何清理此问题的建议。
  • @Alexander-ReinstateMonica 虽然我同意为什么不应该使用单例的一些观点,但在这种情况下,我认为问题更深层次。如果他的单例在测试期间是“它卡在一个状态”,那么它在实际运行时会以同样的方式卡住。换句话说,那个单例具有不可逆的状态——这是一个有问题的设计。
  • 嗯,我没关系。限制系统中状态转换的数量可以简化它。一种方式流程简化了事情,这很好。例如,Shipment 总是从 PendingPackingShippedDelivered。在实际用例中,它永远不会从 Shipped 变为 Pending。如果是这样的话,我认为最好只生成一个新的货物,并让事情朝着“快乐”的方向流动,而不是试图使所有可能的状态转换成为可能(并处理所有需要的测试后果)

标签: swift xctest


【解决方案1】:

我不确定这是一个好方法还是正确方法,但我在想测试我的单例类时使用了它。在您的单元测试类中,考虑为您的单例类添加一个扩展,并在此扩展中添加一个名为 reset 的方法。在此方法中为您的单例实现重置逻辑,并在测试类的 tearDown 函数中调用您的单例的此重置方法。示例代码:

import XCTest
@testable import MyModule

class MySingletonTests {
    override func setUp() {
        //
    } 

    override func tearDown() {
        MySingleton.shared.reset()
    }

    func testSomething() {
        //
    }
}

extension MySingleton {
    func reset() {
        // reset logic
    }
}

【讨论】:

    【解决方案2】:

    单例是依赖注入的一种形式。不是一个伟大的,但仍然是DI。要在不立即更改您的实现的情况下重新获得控制权,请添加一个方法来重置您的单例。它可以改变自己的胆量,也可以释放静态实例。

    然后从tearDown()调用这个方法。

    (然后您可以将单例作为协议传递,而不是让叶子模块伸出手来抓住它。)

    【讨论】:

      【解决方案3】:

      你得到的东西违反了Dependency Inversion Principle

      或者换句话说:

      • 创建一个单例正在实现的协议(只需声明您将用作协议的所有方法和属性,并声明单例类以符合它)

      • 每种需要使用单例的类型要么在其 init 的参数列表中获取一个类型为协议的参数,要么具有一个类型为协议的属性。在这两种情况下,都将单例保存到该属性。

      • 当你创建一个需要单例的对象时,传递它。

      • 您可以使用默认参数,一些非常有用的语法糖。这将允许您传递单例而不需要一遍又一遍地指定它。

      • 在单元测试中,您传递了一个实现协议但仅模拟其功能的对象——即所谓的mock

      【讨论】:

      • 您可以执行 DI,而无需显式地显式注入每个依赖项。他们只是不直接访问单例,而是通过协议抽象引用,允许替换模拟。见my answer here for more details.
      【解决方案4】:

      撇开单例问题不谈,您应该能够在每次测试开始时重新实例化单例,方法是将其设置为 tearDown() 中的 nil(前提是其引用计数为 0)。

      override func tearDown() {
          singleton = nil
          super.tearDown()
      }
      

      您只在运行多个测试时看到此问题的原因是您无法覆盖前一个测试的剩余状态,因为每个测试都有自己的测试类实例。

      对于每个类,测试从运行类设置方法开始。对于每个测试方法,都会分配一个新的类实例并执行其实例设置方法。

      当您自己运行一项测试时,会创建一个测试类实例,并且初始化单例的次数与测试次数相同。但是,当您运行整个测试类时,会为类中的每个测试创建一个测试类实例(5 个测试 = 5 个测试类实例),但单例只初始化一次而不是 5 次。每个额外的测试类实例都是在前一个测试完成运行后创建的,但只要测试类完成运行就会持续存在。对于非单例对象,这不是问题,因为将为每个测试创建一个新的、不相关的实例,但是因为您使用的是单例,相同的实例在测试中仍然存在,并且您没有机会重新初始化除非它被释放。只有第一个测试能够将单例初始化为所需的状态,因此您的所有测试都失败了。

      更多信息请参见here

      【讨论】:

        猜你喜欢
        • 2016-02-04
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-09-09
        • 1970-01-01
        相关资源
        最近更新 更多