Executable Target Testability

I wanted to clarify whether executableTargets, added via the following proposal, support testing? In my experiments, I get "undefined symbol" linker errors for any structs or classes I attempt to access from test that depends on the executableTarget. I can provide a sample project but first wanted to verify whether this is known to be a supported case (if so any examples where this was done?)

Here's the Package.swift

// swift-tools-version:5.4
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "SwiftSampleLambda",
    products: [
        
        //I'm not sure whether I actually need the executable defined here and the executableTarget?
         .executable(
            name: "SwiftSampleLambda",
            targets: ["SwiftSampleLambda"]),
           
    ],
    dependencies: [
         .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", .upToNextMajor(from:"0.4.0")),
    ],
    targets: [
        .executableTarget(
            name: "SwiftSampleLambda",
            dependencies: [
                .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"),
                .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-runtime"),
            ]),
        .testTarget(
            name: "SwiftSampleLambdaTests",
            dependencies: [
                
                //Several different flavors of attempts to depend on the executable target.
                
                //#1: Target
                .target(name: "SwiftSampleLambda"), //Linker errors for any SwiftSampleLambda executable symbols accessed in test.
                
                //#2: Not sure what just a string would refer to but this didn't work either.
                //"SwiftSampleLambda", //Linker errors for any SwiftSampleLambda executable symbols accessed in test.
                
                //#3: Executable product
                //.product(name: "SwiftSampleLambda", package: "SwiftSampleLambda"), //Package errors so this probably isn't right.
                
                .product(name: "AWSLambdaTesting", package: "swift-aws-lambda-runtime"),
            ]),
    ]

They didnā€™t originally, but at some point the plan is for them to support tests. I am not sure when it did or will change, so I will answer for both before and after, and you (and any other reader) will have to try it with your particular toolchain.

Before

Executable targets (or before executableTarget existed separately, any target with a main.swift) did not generate anything linkable, so tests had no means of importing them directly. The common strategy was to reduce the main.swift to a single function call, or extract the @main attribute to an otherwise empty extension in a file of its own. Then that lone file should be declared as the executable target, and all the other source files should be moved into a distinct library target. Tests can then import the library.

After

Tests can directly import the module of the executable. SwiftPM will have also built it as a library by flagging to the compiler that @main should be ignored. (This may not support the older main.swift arrangement though.)


You do not need to declare a product unless you expect there to be client packages depending on it.

The simple string shorthand cascades by first checking for matching local targets (just like .target(name: "SwiftSampleLambda")) and then checking for a dependency where the string matches both the package and product name at once (just like .product(name: "SwiftSampleLambda", package: "SwiftSampleLambda").

Targets cannot depend on products in the same package, so this declaration is invalid.


Declaration #1 is ā€œthe correct oneā€ in that it will ensure SwiftSampleLambda will be built before building the tests. If you plan on interacting with the separate executable in the products folder via Foundation.Process or a similar setā€up, then this declaration is what you would want regardless of what Swift toolchain you are using. For new enough (or possibly only future) toolchains, that declaration is should also be all you need in order to import it in the tests.

@SDGGiesbrecht I appreciate the detailed information and that clarified my issue. I can use the executableTarget with Swift 5.4 / Xcode 12.5.1 but the testing functionality won't work (linker errors). But with Swift 5.5 / Xcode 13, I can test my executableTarget so that appears to be the version that introduced the changes you mentioned.

This functionality is really nice in my opinion as it simplifies the package structure by eliminating an extra target just to aid in testing. I'm looking forward to updating to Xcode 13.0 when I overcome some other unrelated limitations.

1 Like

And if it helps someone in the future (including my future self when I upgrade), here's the code that successfully tests an executableTarget. Again it requires Xcode 13+ / swift 5.5+

Package.swift

// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "TestPackage",
    products: [
    ],
    dependencies: [
        // Dependencies declare other packages that this package depends on.
        // .package(url: /* package url */, from: "1.0.0"),
    ],
    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages this package depends on.
        .executableTarget (
            name: "TestPackage",
            dependencies: []),
        .testTarget(
            name: "TestPackageTests",
            dependencies: ["TestPackage"]),
    ]
)

Structure marked as "Main" Entry-Point

@main

struct TestPackage {
    static func main() {
      print("Hello World")
    }

    public func test() -> String {
        return "Hello World"
    }
}

Test File

    import XCTest
    @testable import TestPackage

    final class TestPackageTests: XCTestCase {
        func testExample() {
            // This is an example of a functional test case.
            // Use XCTAssert and related functions to verify your tests produce the correct
            // results.
            TestPackage().test()
        }
    }