Swift - Unit testing private variables and methods
SwiftUnit TestingSwift 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.
- Go into your project editor and define a new Testing Configuration by duplicating the Debug configuration.
- Edit your scheme's Test action so that the build configuration is Testing.
- 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:
- keep the interface as clean as possible, and
- 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:
- 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 necessarilyTarget
, as I'm using SPM only without Xcode, and it's just aTests
directory), and is hard to create specific cases for individual functions. Depends on how complexscan
is, it's not really a good approach. - 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 notpublic
. But if we specifically mark it asinternal 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:
- 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))]
- 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)