Reputation: 7655
I know that
@testable import MyModule
gives ability to explore non-public members of MyModule
from a "test" (built with "testTarget") module MyModuleTests
.
I need the same functionality in my "non-test" module. Not in production, just in debug mode.
My question is: do you know how to do this?
And related (I think, harder question): what magic is actually happening behind @testable
?
Upvotes: 9
Views: 1731
Reputation: 34255
@testable import <module_name> and -enable-testing
[Swift access modifiers]
[Swift module]
consumer side uses @testable import -> producer side should use `-enable-testing` flag
producer side: enable -enable-testing
Enable Testability(ENABLE_TESTABILITY)
- YESOther Swift Flags(OTHER_SWIFT_FLAGS)
- -enable-testing
consumer side: @testable
internal(default)
and public
access level for class is visible for current module as open
internal(default)
access level for others(struct, enum) is visible for current module as public
If you build test schema(consumer) with @testable
but producer doesn't include -enable-testing
you get
Module '<module_name>' was not compiled for testing
Some experiments:
SomeModule
internal class SomeInternalClass {
internal func foo() { }
}
public class SomePublicClass {
public func foo() { }
}
internal class SomeInternalStruct {
internal func foo() { }
}
internal enum SomeInternalEnum: String {
case foo = "hello world"
}
Tests: If you omit @testable
next errors will occur
import XCTest
@testable import ExperimentsTests
class ExperimentsTestsTests: XCTestCase {
func testExample() throws {
let someInternalStruct = SomeInternalStruct() //Cannot find 'SomeInternalStruct' in scope
someInternalStruct.foo()
let someInternalEnum = SomeInternalEnum(rawValue: "") //Cannot find 'SomeInternalEnum' in scope
SomeInternalEnum.foo //Cannot find 'SomeInternalEnum' in scope
}
class SomePublicSubClass: SomePublicClass { //Cannot inherit from non-open class 'SomePublicClass' outside of its defining module
override func foo() { } //Overriding non-open instance method outside of its defining module
}
class SomeInternalSubClass: SomeInternalClass { //Cannot find type 'SomeInternalClass' in scope
override func foo() { } //Method does not override any method from its superclass
}
}
Upvotes: -1
Reputation: 910
To answer your question, for debugging purposes, you can actually use this. Let's say you have a workspace MyAwesomeWkspace
and a project inside MyAwesomeProject
.
Now, create a new framework
aka module
called MyAwesomeModule
. Inside that module create a non-public class called Person
.
If you try to use the class Person
inside MyAwesomeProject
by doing import MyAwesomeModule
and then something like let p = Person()
you will have an error.
But if you do @testable import MyAwesomeModule
, the magic happens and you can now use the class.
Basically @testable
allows you to test things that you didn't declare public. The annotation only works with import
as you can see it here.
So in order to work, the target is compiled with -enable-testing
so that you can have access to non-public members. At least based on what's here
Because, by default, the debug
build configuration is compiled with -enable-testing
, the example I showed you will work. But if you change the build config to release
, you'll see an error saying Module .. was not compiled for testing
since the release
config is not built with the flag.
The Swift access control model, as described in the Access Control section of The Swift Programming Language (Swift 4), prevents an external entity from accessing anything declared as internal in an app or framework. By default, to be able to access these items from your test code, you would need to elevate their access level to at least public, reducing the benefits of Swift’s type safety.
Xcode provides a two-part solution to this problem:
When you set the Enable Testability build setting to Yes, which is true by default for test builds in new projects, Xcode includes the -enable-testing flag during compilation. This makes the Swift entities declared in the compiled module eligible for a higher level of access. When you add the @testable attribute to an import statement for a module compiled with testing enabled, you activate the elevated access for that module in that scope. Classes and class members marked as internal or public behave as if they were marked open. Other entities marked as internal act as if they were declared public.
More here
Late edit: One of the cool parts of swift is that is open source. So if you want to dive deep into the "magic", check it out: https://github.com/apple/swift
Upvotes: 15