Unit testing a Swift project is quite different from unit testing a project written in Objective-C. If you’re used to the flexibility of the Objective-C runtime, then it may feel as if your hands are tied behind your back. Right?
While access control is a very welcome addition with many benefits, it can complicate unit testing, especially if you’re new to unit testing. You probably know that you can apply the
testable attribute to an import statement in a test target to gain access to entities that are declared internal.
While this is a convenient addition, it doesn’t give you access to private entities in a test target. This brings us to the question of the day. How do you unit test private entities?
The short answer to this question is simple. You cannot access private entities from another module and this also applies to test targets. Plain and simple. That’s what access control is for.
But that isn’t the answer to the question. If you ask how to unit test private entities, then you’re asking the wrong question. But why is that?
Mind Your Own Business
Why do you declare an entity private? What’s your motivation for doing so? Take a look at the following example.
I’d like to unit test the
AccountViewViewModel structure. As you can see, the
AccountViewViewModel struct exposes two internal computed properties and it also defines a private method. The
expiresAtAsString computed property offloads some of its work to the private
parse(date:) method. Testing the internal computed properties is straightforward.
But how do we test the private method? We cannot access the private method from the test target. But why should we unit test the private method? We marked it as private for a reason. Right? And that brings us to the answer to the question we started with. We don’t test private methods.
Unit Testing the Public Interface
By unit testing the public interface of the
AccountViewViewModel struct we automatically or implicitly unit test the private interface of the struct. You have the task to make sure the public interface is thoroughly tested. This means that you need to make sure every code path of the
AccountViewViewModel struct is covered by unit tests. In other words, the suite of unit tests should result in complete code coverage. That includes public, internal, and private entities.
If we enable code coverage in Xcode and we run the unit tests of the
AccountViewViewModelstruct, we can see that some code paths are not executed.
This tells us that the unit tests are incomplete. We can ignore the code path for the fatal error. I never unit test code paths that result in a fatal error, but that largely depends on how you use fatal errors in your projects.
We can increase code coverage for the
AccountViewViewModel struct by adding one more unit test.
Implementation and Specification
It’s important to understand that we’re testing the specification of the
AccountViewViewModelstruct. We’re not testing its implementation. While this may sound similar, it’s actually very different. We’re testing the functionality of the
AccountViewViewModel struct. We’re not interested in how it does its magic under the hood.
The key takeaway is that private entities don’t need to be unit tested. Unit testing is a form of black-box testing. This means that we don’t test the implementation of the
AccountViewViewModel struct, we test its specification.
This doesn’t mean that we’re not interested in the implementation, though. We need to make sure the suite of unit tests covers every code path of the entity we’re testing. Code coverage reports are invaluable to accomplish this.