How to expose internal methods of a Swift package written in Objective-C for the tests written in Swift?

Luk*_*asz 6 testing objective-c ios swift swift-package-manager

I am porting legacy Objective-C library to the Swift Package, which has Unit Tests written in Swift. Previously, this library was written using standard approach: Framework target / Xcode project, so the test target could import internal headers (using bridging header). It seems as Swift Package manager does not offer anything like this, therefore testing internals looks problematic (without exposing them to the public).

BTW: @testable import ... does not work for Objective-C code.

I also tried to cheat it with exposing them using:

cSettings: [.headerSearchPath("../ObjcPackage/Source/Internal")]
Run Code Online (Sandbox Code Playgroud)

But it does not work.

I am using swift-tools-version:5.3

Repo demonstrating the problem:

https://github.com/lukaszmargielewski/ObjcPackage

Some snippets:

1. Package definition:

// swift-tools-version:5.3
// Package.swift

import PackageDescription

let package = Package(
    name: "ObjcPackage",
    platforms: [
        .iOS(.v10)
    ],
    products: [
        .library(name: "ObjcPackage", targets: ["ObjcPackage"]),
    ],
    targets: [
        .target(
            name: "ObjcPackage",
            path: "ObjcPackage/Source",
            publicHeadersPath: "Public",
            cSettings: [
                .headerSearchPath("Public"),
                .headerSearchPath("Internal"),
            ]
        ),
        .testTarget(
            name: "ObjcPackageTests",
            dependencies: ["ObjcPackage"],
            path: "ObjcPackageTests",
            sources: ["Source"],
            cSettings: [
                .headerSearchPath("../ObjcPackage/Source/Internal")
            ]
        )
    ]
)
Run Code Online (Sandbox Code Playgroud)

2. Public header - ObjcPackage

// ObjcPackage.h
#import <Foundation/Foundation.h>

@interface ObjcPackage : NSObject

- (nullable instancetype)initWithTitle:(nonnull NSString *)title NS_DESIGNATED_INITIALIZER;

- (nonnull instancetype)init NS_UNAVAILABLE;

@property (nonnull, readonly, nonatomic, copy) NSString *title;

@end
Run Code Online (Sandbox Code Playgroud)

3. Internal header - ObjcPackage

// ObjcPackage+Internal.h
#import <Foundation/Foundation.h>
#import "ObjcPackage.h"

/// Internal methods of ObjcPackage
@interface ObjcPackage ()

- (NSString *)generateInternalSecret;

@end
Run Code Online (Sandbox Code Playgroud)

4. Implementation file - ObjcPackage

// ObjcPackage.m
#import "ObjcPackage.h"
#import "ObjcPackage+Internal.h"

@interface ObjcPackage()

@property (nonatomic, readwrite, copy) NSString *title;

@end

@implementation ObjcPackage

- (instancetype)initWithTitle:(NSString *)title {
    self = [super init];
    if (self != nil) {
        self.title = title;
    }
    return self;
}

- (NSString *)generateInternalSecret {
    return [NSString stringWithFormat:@"%@_internal_secret", self.title];
}

@end
Run Code Online (Sandbox Code Playgroud)

5. Test example (ObjcPackageTests)

// ObjcPackageTests.swift
import Foundation
import XCTest
@testable import ObjcPackage

class ObjcPackageTests: XCTestCase {

    private lazy var objcPackage = ObjcPackage(title: "A")

    func testPublic() {
        XCTAssertEqual(objcPackage?.title, "A")
    }

    func testInternal() {
        // How to expose internal Objective-C header to the test package, 
        // without making it public?
        // !!! - COMPILATION ERROR: - !!!
        XCTAssertEqual(objcPackage.generateInternalSecret, "A_internal_secret")
    }
}
Run Code Online (Sandbox Code Playgroud)