67

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?

Cristik
  • 30,989
  • 25
  • 91
  • 127
breaktop
  • 1,899
  • 4
  • 37
  • 58
  • 1
    See http://programmers.stackexchange.com/questions/750/what-should-you-test-with-unit-tests – zneak May 24 '16 at 18:30
  • As a game-programmer, I do different things than Unit Tests but they help a ton in personal projects. For ex: guard against non-existing operations (debugAssert and log). Ensure functions can only have their proper ranges passed in. switch default a "never get here" assert. Decide if that safety is on the function or caller. Those always work in private functions. Moreso if you want to provide a default value for nonexisting operations, as the crash, yeah use TDD. But TDD won't protect you from production flaws the same way defaulting and guarding against will save the user a crash or twelve – Stephen J Dec 28 '19 at 01:44

8 Answers8

53

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

Daniel Galasko
  • 23,617
  • 8
  • 77
  • 97
47

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.

Cristik
  • 30,989
  • 25
  • 91
  • 127
  • 1
    Say for example I test only the public method ```performOperation``` to see if the behaviour is correct. This would be I'm not testing all the different operation implementation in the ```operations``` ```Dictionary```. Would't that be a bad idea as i'm not testing the different operation implementation. Wouldn't that be a bad idea? – breaktop May 24 '16 at 19:24
  • @breaktop - the input provided to the public method `performOperation` can cover all the various operations. If one or more input doesn't yield the expected output, you know which private implementations are broken without having to test them explicitly. You can still write the test on public `performOperation` & test various inputs, then adjust private implementation as required based on the results. – mc01 May 24 '16 at 20:34
  • 1
    @breaktop if you add enough tests for `performOperation` to exercise all code paths in the private `operations` dictionary, then you will have full coverage over the private member. – Cristik May 25 '16 at 04:40
  • 9
    Unit Testing is not necessarily black box testing. http://stackoverflow.com/a/24627489/1202880 http://stackoverflow.com/a/9893055/1202880 – sbrun Mar 20 '17 at 03:57
  • 1
    @Altece the problem with non black box unit tests is the fact that they make refactoring harder as they are aware and rely on implementation details of the unit. – Cristik Oct 10 '17 at 08:22
  • @Cristik I share your concern, but that doesn't make them any less useful. There are different kinds of testing for a reason—each technique has its pros and cons. The Wikipedia page for [White-box testing](https://en.wikipedia.org/wiki/White-box_testing) explicitly mentions unit tests, along with advantages and disadvantages. I'm a "use the right tool" kind of guy, and sometimes fragile white-box unit tests happen to be appropriate. – sbrun Oct 18 '18 at 22:44
46

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.

kuzdu
  • 7,124
  • 1
  • 51
  • 69
Diego Freniche
  • 5,225
  • 3
  • 32
  • 45
  • I'm sure this is a ridiculous question, but as long as it's a hack...why not put the extension in the SAME file as TestTests (since the private method(s) should be tested from only one place)? Plus, #if DEBUG is already set implicitly in the unit test. I understand that if it was production code, the extension would go in its own file, but it's not. Seems to me the benefits of testing with this method outweigh changing the visibility in the production class, testing only the higher-level broader-scoped interface, or keeping the code elegant but not directly testable. – Glenn Jan 18 '19 at 21:17
  • 2
    This worked great to expose the private initialiser of my singleton so I could have a fresh instance for each test. Singletons aren't the best for tests, but given this was code already written it was better for now to make the singleton testable. I will have to disagree about the code comment in the answer as Swift 4 requires the extension to be in the same file to have access to the private functions from within an extension. – DonnaLea Mar 22 '19 at 17:12
  • 1
    Glenn, the answer is: because extensions in a different file don't have access to private members. – Torrontés Mar 30 '23 at 19:04
12

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.

matt
  • 515,959
  • 87
  • 875
  • 1,141
3

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.

superarts.org
  • 7,009
  • 1
  • 58
  • 44
1

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".

lmiguelvargasf
  • 63,191
  • 45
  • 217
  • 228
Joey Zhou
  • 45
  • 5
  • I voted it up 2 years ago, and now I realized that I don't like this answer anymore (no offense! More towards the original article). The reason is that it makes sense to a) keep the interface as clean as possible, and b) limit access level as private as possible. And to achieve these goals, we are likely to have a bunch of private functions, but we still want to unit test them. For example, I have a class that has `doSomething(filename: String)`, and I want it to be the only public functions. It makes perfect sense. But I also want to make sure every private bits and pieces to work as expec... – superarts.org Feb 03 '21 at 16:20
  • ... as expected, because the logic inside `doSomething` can be a lot and I don't want to create a bunch of test files if I can provide individual strings to test. Of course, Swift doesn't allow us to do this (I hope there could be some @testable modifier at function level), so probably I'll end up with `internal` as a workaround. But just saying "unit testing should be a black box" doesn't solve real life problems. – superarts.org Feb 03 '21 at 16:26
1

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

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)

MrWoWander
  • 11
  • 3