【问题标题】:Swift - Unit testing private variables and methodsSwift - 单元测试私有变量和方法
【发布时间】:2016-05-24 18:29:14
【问题描述】:

我正在尝试测试一门课程,但我对要测试的内容有点困惑。这是我要单元测试的类:

class CalculatorBrain {

    private var accumulator = 0.0

    func setOperand(operand: Double) {
        accumulator = operand
    }

    var result: Double {
        return accumulator
    }

    private var operations: Dictionary<String, Operation> = [
        "=" : .Equals,

        "π" : .Constant(M_PI),
        "e" : .Constant(M_E),

        "±" : .UnaryOperation({ (op1: Double) -> Double in return -op1 }),
        "√" : .UnaryOperation(sqrt ),
        "cos": .UnaryOperation(cos),

        "+" : .BinaryOperation({ (op1: Double, op2: Double) -> Double in return op1 + op2 }),
        "−" : .BinaryOperation({ (op1: Double, op2: Double) -> Double in return op1 - op2 }),
        "×" : .BinaryOperation({ (op1: Double, op2: Double) -> Double in return op1 * op2 }),
        "÷" : .BinaryOperation({ (op1: Double, op2: Double) -> Double in return op1 / op2 })
    ]

    private enum Operation {
        case Constant(Double)
        case UnaryOperation((Double) -> Double)
        case BinaryOperation((Double, Double) -> Double)
        case Equals
    }

    func performOperation(symbol: String) {
        if let operation = operations[symbol] {
            switch operation {
            case .Constant(let value):
                accumulator = value
            case .UnaryOperation(let function):
                accumulator = function(accumulator)
            case .BinaryOperation(let function):
                executePendingBinaryOperation()
                pendingBinaryOperation = PendingBinaryOperationInfo(binaryOperation: function, firstOperand: accumulator)
            case .Equals:
                executePendingBinaryOperation()
            }
        }
    }

    private var pendingBinaryOperation: PendingBinaryOperationInfo?

    private struct PendingBinaryOperationInfo {
        var binaryOperation: (Double, Double) -> Double
        var firstOperand: Double
    }

    private func executePendingBinaryOperation() {
        if let pending = pendingBinaryOperation {
            accumulator = pending.binaryOperation(pending.firstOperand, accumulator)
            pendingBinaryOperation = nil
        }
    }
}

对于上面的代码,什么是好的测试。

是否值得测试字典 operations 中的每个操作(+、-、*、/ 等)?

是否值得测试私有方法?

【问题讨论】:

  • 作为一名游戏程序员,我做的事情与单元测试不同,但它们对个人项目有很大帮助。例如:防范不存在的操作(debugAssert 和日志)。确保函数只能传入正确的范围。将默认值切换为“永远不会到达这里”断言。确定该安全性是否在函数或调用者上。那些总是在私人职能中工作。此外,如果您想为不存在的操作提供默认值,例如崩溃,请使用 TDD。但是 TDD 无法保护您免受生产缺陷的影响,就像默认和防范会为用户节省一次或十二次崩溃一样

标签: swift unit-testing


【解决方案1】:

您不能在 Swift 中使用 @testable 测试私有方法。您只能测试标记为internalpublic 的方法。正如文档所说:

注意:@testable 仅提供对“内部”函数的访问; “私人”声明在文件之外是不可见的,即使 使用@testable。

阅读更多here

【讨论】:

    【解决方案2】:

    虽然我同意不测试private 的东西,而且我宁愿只测试公共接口,但有时我需要测试隐藏的类中的某些东西(比如复杂的状态机)。对于这些情况,您可以做的是:

    import Foundation
    
    public class Test {
    
        internal func testInternal() -> Int {
            return 1
        }
    
        public func testPublic() -> Int {
            return 2
        }
    
        // we can't test this!        
        private func testPrivate() -> Int {
            return 3
        }
    }
    
    // won't ship with production code thanks to #if DEBUG
    // add a good comment with "WHY this is needed ?"
    #if DEBUG
    extension Test {
        public func exposePrivate() -> Int {
            return self.testPrivate()
        }
    }
    #endif
    

    那么你可以这样做:

    import XCTest
    @testable import TestTests
    
    class TestTestsTests: XCTestCase {
    
        func testExample() {
            let sut = Test()
    
            XCTAssertEqual(1, sut.testInternal())
        }
    
        func testPrivateExample() {
            let sut = Test()
    
            XCTAssertEqual(3, sut.exposePrivate())
        }
    }
    

    我完全理解这是一个 hack。但是知道这个技巧可以在将来保存你的培根。不要滥用这个技巧。

    【讨论】:

    • 我确定这是一个荒谬的问题,但只要它是一个黑客......为什么不将扩展名放在与 TestTests 相同的文件中(因为应该测试私有方法只有一个地方)?另外,#if DEBUG 已经在单元测试中隐式设置。我知道如果它是生产代码,扩展名会放在它自己的文件中,但事实并非如此。在我看来,使用这种方法进行测试的好处超过了更改生产类中的可见性、仅测试更高级别、范围更广的接口,或者保持代码优雅但不能直接测试。
    • 这很好地暴露了我的单例的私有初始化程序,这样我就可以为每个测试创建一个新实例。单例并不是最适合测试的,但鉴于这是已经编写的代码,现在最好让单例可测试。我将不得不不同意答案中的代码注释,因为 Swift 4 要求扩展位于同一文件中才能从扩展中访问私有函数。
    【解决方案3】:

    单元测试应该被视为黑盒测试,这意味着您不关心所测试单元的内部结构。您主要感兴趣的是根据您在单元测试中提供的输入来查看单元输出是什么。

    现在,通过输出我们可以断言几件事:

    • 方法的结果
    • 作用于对象后的状态,
    • 与对象所具有的依赖项的交互

    在所有情况下,我们只对公共接口感兴趣,因为它是与世界其他地方通信的接口。

    私人物品不需要进行单元测试,因为任何私人物品都被公共物品间接使用。诀窍是编写足够多的测试来锻炼公共成员,以便完全覆盖私有成员。

    另外,要记住的一件重要事情是单元测试应该验证单元规范,而不是它的实现。验证实现细节增加了单元测试代码和被测试代码之间的紧密耦合,这有一个很大的缺点:如果测试的实现细节发生变化,那么单元测试很可能也需要改变。

    以黑盒方式编写单元测试意味着您可以重构这些单元中的所有代码,而不必担心通过更改测试可能会在单元测试代码中引入错误。不可靠的单元测试有时比缺乏测试更糟糕,因为给出误报的测试可能会隐藏代码中的实际错误。

    【讨论】:

    • 比如说我只测试公共方法performOperation 看行为是否正确。这将是我没有在operationsDictionary 中测试所有不同的操作实现。这不是一个坏主意,因为我没有测试不同的操作实现。这不是个坏主意吗?
    • @breaktop - 提供给公共方法performOperation 的输入可以涵盖所有各种操作。如果一个或多个输入没有产生预期的输出,您无需显式测试即可知道哪些私有实现被破坏。您仍然可以在公共performOperation 上编写测试并测试各种输入,然后根据结果根据需要调整私有实现。
    • @breaktop 如果您为performOperation 添加足够的测试以执行私有operations 字典中的所有代码路径,那么您将完全覆盖私有成员。
    • @Altece 非黑盒单元测试的问题在于,它们使重构变得更加困难,因为他们知道并依赖于单元的实现细节。
    【解决方案4】:

    迭戈的回答很聪明,但可以走得更远。

    1. 进入您的项目编辑器并通过复制调试配置来定义新的测试配置。
    2. 编辑方案的测试操作,使构建配置为测试。
    3. 现在编辑您的测试目标的构建设置,为测试配置定义一个额外的活动编译条件值"TESTING"

    现在您可以说 #if TESTING,与单纯的 DEBUG 不同。

    例如,我使用它来声明只有测试才能看到的初始化程序。

    【讨论】:

      【解决方案5】:

      我发现this link 与 Cristik 有类似的说法。

      基本上,您问错了问题,您不应该试图测试标有“私有”的类/函数。

      【讨论】:

      • 我在 2 年前投了赞成票,现在我意识到我不再喜欢这个答案了(无意冒犯!更多关于原始文章的内容)。原因是 a) 保持界面尽可能干净,b) 尽可能限制访问级别是有意义的。而要实现这些目标,我们很可能会拥有一堆私有函数,但我们仍然想对它们进行单元测试。例如,我有一个具有doSomething(filename: String) 的类,我希望它是唯一的公共函数。这很有意义。但我也想确保每个私人点点滴滴都能按预期工作......
      • ... 正如预期的那样,因为doSomething 内部的逻辑可能很多,如果我可以提供单独的字符串进行测试,我不想创建一堆测试文件。当然,Swift 不允许我们这样做(我希望在函数级别可以有一些 @testable 修饰符),所以我最终可能会使用 internal 作为解决方法。但是仅仅说“单元测试应该是一个黑匣子”并不能解决现实生活中的问题。
      【解决方案6】:

      我认为实际上不需要测试私人成员。 但是如果你想在UnitTest使用private成员(属性和方法),有一种方法可以使用Protocol

      Protocol PrivateTestable {
        associatedtype PrivateTestCase  
        var privateTestCase: PrivateTestCase {get}
      }
      

      并尝试在同一个文件(目标类文件)中扩展协议。

      extension CalculatorBrain: PrivateTestable {
        struct PrivateTestCase {
          private let target: CalculatorBrain
      
          var pendingBinaryOperation: PendingBinaryOperationInfo? {
            return target.pendingBinaryOperation
          }
      
          init(target: CalculatorBrain) {
            self.target = target
          }
        }
      
        var privateTestable: PrivateTestCase { 
          return PrivateTestCase(target: self)
        }
      
      }
      

      那么你可以在UnitTest中使用pendingBinaryOperation

      class CalculatorBrainTest: XCTestCase {
        func testPendingBinaryOperation() {
          let brain = CalculatorBrain()
          XCTAssertNotNil(brain.privateTestCase.pendingBinaryOperation)
        }
      }
      

      【讨论】:

        【解决方案7】:

        简短的回答是你不能。不能测试私处。

        但是,我不认为“你不应该”是一个有效的答案。我曾经这样想,但现实生活中的场景比我们想象的要复杂得多。在某些时候,我需要编写一个FileScanner 类作为框架的一部分,它符合一个只有scan(filename: String) 函数的Scanner 协议。当然FileScanner.scan(filename: String)必须是public,但是支持scan的功能呢?

        正如我在上面的评论中提到的,我想:

        1. 尽可能保持界面整洁,并且
        2. 尽可能将访问级别限制为私密

        这意味着我不想公开其他类未使用的其他功能。我真的希望在函数级别有一个 @testable 修饰符(类似于 @discardable 等),但由于它在 Swift 中并不存在,不幸的是我们只有 2 个选项:

        1. 只为scan 编写单元测试,这是大多数人的建议。这需要单元测试包中的大量输入文件(不一定是Target,因为我只使用没有Xcode 的SPM,而且它只是一个Tests 目录),并且很难为单个函数创建特定的案例。取决于scan 的复杂程度,这并不是一个好方法。
        2. 公开其他私有函数。我最终采用了这种方法,并制定了一个约定,如果一个函数没有任何修饰符,我们假设它是internal,并且可以被同一个包(目标)中的其他文件使用,而不是public。但如果我们特别将其标记为internal func 等,则意味着我们只想将其设为@testable,并且它不应该被同一包中的其他类使用。

        所以,我的结论是,即使你还不能在 Swift 中测试私有方法和属性,我认为这是 Swift 的一个限制,但不是一个无效的用例。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2019-09-27
          • 2014-08-10
          • 2010-11-08
          • 1970-01-01
          • 1970-01-01
          • 2013-11-09
          相关资源
          最近更新 更多