Recently I was working on a project that includes a dynamic framework as a target. This dynamic framework has evolved over the years from a static library with Objective-C code to a hybrid Swift and Objective-C dynamic framework.

The project also has an XCTest unit test target with Objective-C and Swift. In the test target, I was trying to test a swift class that I introduced. This class was mostly private and internal with some public functions in it. In one of the tests it was needed to access a method with internal functionality so I changed the import to @testable import. Ran the tests...and got an error with accessing the internal header.

Everything looked fine to me. I cleared the derived data and removed the build folders, cleaned and ran the tests again. Still failed. Now it was time to start digging into the project structure and build settings.

Since the project is almost five years old, it came through numerous waves of refactoring, by modernising the Objective-C syntax, to the introduction of swift. These refactoring also changed some of the build settings in the Xcode project itself. That is where I started my investigation.

First, I made sure that the Xcode build settings responsible for turning on framework modules are correct. The key responsible for it is CLANG_ENABLE_MODULES. Sure enough, it was turned on.

//:configuration = Debug

//:configuration = Release

//:completeSettings = some

Next, I checked that testability is enabled for the dynamic framework. Enabling and disabling is done by setting ENABLE_TESTABILITY

//:configuration = Debug

//:configuration = Release

//:completeSettings = some

Ok, testability was on for debug, which is what is important for unit tests.

I was really confused at this stage. All the settings required for enabling modules and testability are set.

To actually find the issue, I had to remember and recall how the dynamic framework project evolved over the years. As part of the evolution, Swift was introduced pragmatically, starting by the tests and then making its way to the actual framework work. In this evolution, we had introduced a bridging header to the test target, that was done to give the swift unit test files some access to some Objective-C unit test files.

When opening the bridging header, it was clear which line was the culprit.

#import "SomeUtilityHeader.h"
#import "IDontKnowWhatThisHeaderIsDoing.h"
#import "MyDynamicFramework.h"
#import "SomeOtherUtilityHeader.h"

In the header, I was importing the dynamic framework using the Objective-C header import that was importing the module without exposing the internal functions. Since the bridging headers are compiled before swift files when building the test targets. The @testable import was preceded by the #import "MyDynamicFramework.h" from the bridging header and that was overwriting the second import.

After removing that line from the bridging header, tests start running and everything was back to normal.