Swift-单元测试私有变量和方法



我试图测试一个类,但我有点困惑于测试什么。这是我想要单元测试的类:

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中的每一个操作(+、-、*、/等)?

值得测试私人方法吗?

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

注意:@testable只为"内部"函数提供访问权限;即使在使用@testable。

点击此处阅读更多

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

现在,通过输出,我们可以断言以下几点:

  • 方法的结果
  • 物体作用在其上之后的状态
  • 与对象的依赖项的交互

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

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

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

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

虽然我同意不测试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())
}
}

我完全理解这是一次黑客攻击。但知道这个技巧可以在未来拯救你的培根。不要滥用这个把戏。

Diego的回答很聪明,但也有可能更进一步。

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

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

例如,我使用它来声明只有测试才能看到的初始值设定项。

简单的答案是你不能。私人零件无法测试。

然而,我不认为"你不应该"是一个有效的答案。我以前是这样想的,但现实生活中的场景比我们预期的要复杂。在某种程度上,我需要编写一个FileScanner类作为框架的一部分,它符合只有scan(filename: String)函数的Scanner协议。当然,FileScanner.scan(filename: String)需要是public,但是支持scan的函数呢?

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

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

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

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

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

我发现了这个链接,它与Cristik说了一些类似的话。

基本上,你问错了问题,你不应该试图测试标有"private"的类/函数。

我认为实际上不需要测试私有成员。但是,如果要在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)
}
}

如果你真的想在测试中获得一个私有字段,你可以使用Mirror类:

let testClass = CalculatorBrain()
let mirror = Mirror(reflecting: testClass)
func extract<T>(variable name: StaticString, mirror: Mirror?) -> T? {
guard let mirror = mirror else {
return nil
}
guard let descendant = mirror.descendant("(name)") as? T
else {
return extract(variable: name, mirror: mirror)
}

return descendant
}
let result: Dictionary<String, Any>? = extract(variable: "operations", mirror: mirror)
print(result!)

例如,我对类进行了扩展,以检查输出结果

extension CalculatorBrain {
var test: Any {
operations
}
}
print("")
print(testClass.test)

结果,我得到了这个:

  1. 镜像
["−": __lldb_expr_24.CalculatorBrain.Operation.BinaryOperation((Function)),
"√": __lldb_expr_24.CalculatorBrain.Operation.UnaryOperation((Function)),
"+": __lldb_expr_24.CalculatorBrain.Operation.BinaryOperation((Function)),
"÷": __lldb_expr_24.CalculatorBrain.Operation.BinaryOperation((Function)),
"e": __lldb_expr_24.CalculatorBrain.Operation.Constant(2.718281828459045),
"π": __lldb_expr_24.CalculatorBrain.Operation.Constant(3.141592653589793),
"cos": __lldb_expr_24.CalculatorBrain.Operation.UnaryOperation((Function)),
"=": __lldb_expr_24.CalculatorBrain.Operation.Equals, 
"±": __lldb_expr_24.CalculatorBrain.Operation.UnaryOperation((Function)),
"×": __lldb_expr_24.CalculatorBrain.Operation.BinaryOperation((Function))]
  1. 扩展
["×": __lldb_expr_24.CalculatorBrain.Operation.BinaryOperation((Function)),
"÷": __lldb_expr_24.CalculatorBrain.Operation.BinaryOperation((Function)), 
"√": __lldb_expr_24.CalculatorBrain.Operation.UnaryOperation((Function)), 
"=": __lldb_expr_24.CalculatorBrain.Operation.Equals,
"−": __lldb_expr_24.CalculatorBrain.Operation.BinaryOperation((Function)),
"±": __lldb_expr_24.CalculatorBrain.Operation.UnaryOperation((Function)),
"e": __lldb_expr_24.CalculatorBrain.Operation.Constant(2.718281828459045), 
"cos": __lldb_expr_24.CalculatorBrain.Operation.UnaryOperation((Function)),
"π": __lldb_expr_24.CalculatorBrain.Operation.Constant(3.141592653589793), 
"+": __lldb_expr_24.CalculatorBrain.Operation.BinaryOperation((Function))]

私有方法不会被测试(至少我不知道如何在不更改主代码的情况下做到这一点)

最新更新