Swift - Unit testing private variables and methods

SwiftUnit Testing

Swift Problem Overview


I'm trying to test a class but I'm kind of confused as to what to test. Here is the class I want to unit test:

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
        }
    }
}

For the code above, what would be good tests.

Is it worth testing every single operation (+, -, *, /, etc) in the dictionary operations?

Is it worth testing the private methods?

Swift Solutions


Solution 1 - Swift

You can't test private methods in Swift using @testable. You can only test methods marked either internal or public. As the docs say:

> Note: @testable provides access only for “internal” functions; > “private” declarations are not visible outside of their file even when > using @testable.

Read more here

Solution 2 - Swift

Unit testing should be considered black box testing, which means you don't care about the internals of the unit you test. You are mainly interested to see what's the unit output based on the inputs you give it in the unit test.

Now, by outputs we can assert on several things:

  • the result of a method
  • the state of the object after acting on it,
  • the interaction with the dependencies the object has

In all cases, we are interested only about the public interface, since that's the one that communicates with the rest of the world.

Private stuff don't need to have unit tests simply because any private item is indirectly used by a public one. The trick is to write enough tests that exercise the public members so that the private ones are fully covered.

Also, one important thing to keep in mind is that unit testing should validate the unit specifications, and not its implementation. Validating implementation details adds a tight coupling between the unit testing code and the tested code, which has a big disadvantage: if the tested implementation detail changes, then it's likely that the unit test will need to be changed also.

Writing unit tests in a black box manner means that you'll be able to refactor all the code in those units without worrying that by also having to change the tests you risk into introducing bugs in the unit testing code. Unreliable unit tests are sometimes worse than a lack of tests, as tests that give false positives are likely to hide actual bugs in your code.

Solution 3 - Swift

Although I agree with not testing private stuff, and I'd rather prefer to test just public interface, sometimes I've needed to test something inside a class that was hidden (like a complex state machine). For these cases what you can do is:

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

Then you can do this:

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())
    }
}

I understand perfectly that this is a hack. But knowing this trick can save your bacon in the future or not. Do not abuse this trick.

Solution 4 - Swift

Diego's answer is clever but it is possible to go further.

  1. Go into your project editor and define a new Testing Configuration by duplicating the Debug configuration.
  2. Edit your scheme's Test action so that the build configuration is Testing.
  3. Now edit your test target's build settings to define an additional Active Compilation Conditions value for the Testing configuration, "TESTING".

Now you can say #if TESTING, as distinct from mere DEBUG.

I use this, for example, to declare initializers that only a test can see.

Solution 5 - Swift

I found this link which is saying something similar with Cristik.

Basically, you are asking the wrong question, you should not be seeking to test the class/functions marked with "private".

Solution 6 - Swift

I think actually don’t need to test of private members. But if you want to use to private members(properties & methods) at UnitTest, there is a way that use Protocol.

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

And try to extension the protocol in same file (target class file).

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)
  }

}

Then you can use pendingBinaryOperation in UnitTest

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

Solution 7 - Swift

Short answer is you can't. Private parts can't be tested.

However, I don't think "you shouldn't" is a valid answer. I used to think in this way, but real life scenarios are more complicated than we would expect. At some point, I need to write a FileScanner class as part of a framework, which conforms to a Scanner protocol that only has a scan(filename: String) function. Of course FileScanner.scan(filename: String) needs to be public, but how about the functions that support scan?

As I mentioned in a comment above, I want to:

  1. keep the interface as clean as possible, and
  2. limit access level as private as possible

Which means I don't want to expose other functions that are not used by other classes. I really hope there's a @testable modifier at function level (works like @discardable etc) but since it's not really there in Swift, we unfortunately only have 2 options:

  1. Write unit tests for scan only, which is suggested by most people. This requires a lot of input files in the unit test bundle (not necessarily Target, as I'm using SPM only without Xcode, and it's just a Tests directory), and is hard to create specific cases for individual functions. Depends on how complex scan is, it's not really a good approach.
  2. Expose private other functions. I ended up with this approach, and make a convention, that if a function doesn't have any modifier, we assume it's internal and can be used by other files in the same bundle (Target), just not public. But if we specifically mark it as internal func etc, it means we just want to make it @testable and it should never be used by other classes in the same bundle.

So, my conclusion is that even you can't test private methods and properties in Swift yet, I consider it as a limitation of Swift but not an invalid use case.

Solution 8 - Swift

If you really want to get a private field in tests, you can use the Mirror class:

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!)

For example, I made an extension of the class to check the output result

extension CalculatorBrain {
    var test: Any {
        operations
    }
}

print("")

print(testClass.test)

As a result, I got this:

  1. Mirror
["−": __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. Extension
["×": __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))]

Private methods will not be tested (at least I do not know how to do this without changing the main code)

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionbreaktopView Question on Stackoverflow
Solution 1 - SwiftDaniel GalaskoView Answer on Stackoverflow
Solution 2 - SwiftCristikView Answer on Stackoverflow
Solution 3 - SwiftDiego FrenicheView Answer on Stackoverflow
Solution 4 - SwiftmattView Answer on Stackoverflow
Solution 5 - SwiftJoey ZhouView Answer on Stackoverflow
Solution 6 - SwiftInKwon Devik KimView Answer on Stackoverflow
Solution 7 - Swiftsuperarts.orgView Answer on Stackoverflow
Solution 8 - SwiftMrWoWanderView Answer on Stackoverflow