Scheme "My-LondonApp" is not testable

Hello,

During the migration process from an Xcode-managed project (My-Project.xcodeproj ) to a Tuist-managed project (My-Project-Tuist.xcodeproj ), I’ve encountered an issue with migrating tests.

When tapping the “Test” button in Xcode, I receive the following alert:
Scheme “My-LondonApp” is not testable.

Tuist Version:

[tools]
tuist = "4.43.2"

Directory Structure:



MyRepo/
├── Tuist/
│  ...
│
├── Tuist.swift 
│
│
├── Projects/
	├── App
	│	├── Project.swift
	│	│
	│	├── My-Project-Tuist.xcodeproj
	│	├── My-Project
	│		├── App
	│		│	├─ Resources
	│		│	│	├─ Images
	│		│	│	├─ ...
	│		│	│	├─ info.plist
	│		│	│
	│		│	│
	│		│	├─ Sources
	│		│	│	├─ Application.swift
	│		│	│	├─ ...
	│		│	
	│		│	
	│		├─ Tests
	│			├─ TestPlans
	│			│	├─ AllProjectTests.xctestplan
	│			│	├─ IntegrationTests.xctestplan
	│			│	├─ UnitTests.xctestplan
	│			│
	│			├─ UnitTests
	│			│	├─ Resources
	│			│	│	├─ ...
	│			│	│
	│			│	├─ Sources
	│			│	│	├─ ...
	│			│	│	
	│			├─ IntegrationTests
	│			│	├─ Resources
	│			│	│	├─ ...
	│			│	│
	│			│	├─ Sources
	│			│	│	├─ ...
	│			│	│	
	│			├─ UITests
	│			│	├─ Resources
	│			│	│	├─ ...
	│			│	│
	│			│	├─ Sources
	│			│	│	├─ ...
	│			│	│	
	│
	│
	│
	├── Modules
		├── Project.swift
		│
		├── My-Modules-Tuist.xcodeproj
		├── My-Modules
		...


Requirements:

According to our internal requirements
we want to have separate test targets for UnitTests and IntegrationTests (and UI tests):

  • “My-LondonApp” - (Main App target) app target for App Store
  • “My-LondonAppUnitTests” - ( product: .unitTests,) target. “My-LondonApp” is host target
  • “My-LondonAppIntegrationTests” - ( product: .unitTests,) target. “My-LondonApp” is host target

App Target Definition in Tuist:

So thats why I define target in Tuist’s manifest next:
(targetConfig.name contains “My-LondonApp”)


let result: Target = .target(
            name: targetConfig.name,
            destinations: My-ProjectConf.destinations,
            product: .app,
            productName: targetConfig.productName,
            bundleId: targetConfig.bundleId,
            deploymentTargets: My-ProjectConf.deploymentTargets,
            infoPlist: .file(path: "My-Project/App/Resources/Info.plist"),
            sources: [
                "My-Project/App/Sources/**",
            ],
            
            resources:
                    .resources(
                        [
                            "My-Project/App/Resources/Sounds/**",
                            "My-Project/App/Resources/My-Project.sqlite",
                            "My-Project/App/Sources/**/*.storyboard",
                            "My-Project/App/Sources/**/*.xib",
                            ...
                        ],
                        privacyManifest: PrivacyInfo.makePrivacyManifest(type: targetConfig.privacyManifestType)
                    ),
            headers: .headers(private: "My-Project/App/Sources/**"),
            
            entitlements: entitlementsFile(from: "My-Project/App/Resources/Entitlements.plist"),
            
            scripts: appTargetScripts(),
            dependencies: [
                .external(name: "GoogleTagManager"),
                
                // https://firebase.google.com/docs/ios/setup#available-pods
                .external(name: "FirebaseCore"),

                // local Modules
                .project(target: "NetworkKit", path: "../Modules"),
            ],
            settings: appTargetSettings(config: targetConfig)
        )

and build schema (what I understand, If I want use tests i need to create custom schema)

let result: Scheme =
    .scheme(
        name: targetConfig.name,
        shared: true,
        buildAction: .buildAction(targets: [
            .target(targetConfig.name)
        ]),
        testAction: .testPlans([
                .relativeToManifest("My-Project/Tests/AllProjectTests.xctestplan")
        ])
    )

return result

the most interesting, targets of tests:

target for UnitTests

let result: Target = .target(
            name: "\(hostTargetConfig.name)UnitTests",
            destinations: My-ProjectConf.destinations,
            product: .unitTests,
            bundleId: "\(hostTargetConfig.bundleId)UnitTests",
            deploymentTargets: My-ProjectConf.deploymentTargets,
            infoPlist: .default,
            sources: [
                "My-Project/Tests/UnitTests/Sources/**",
            ],
            resources: [
                "My-Project/Tests/UnitTests/Resources/**",
            ],
            dependencies: [
                .target(name: hostTargetConfig.name),
                .xctest,
            ],
            settings: Self.testsTargetSettings()
        )
        return result

Scheme for UnitTestTarget

let testAction: ProjectDescription.TestAction? = .testPlans([
    .relativeToManifest("My-Project/Tests/UnitTests.xctestplan")
])

let result: Scheme =
    .scheme(
        name: "\(hostTargetConfig.name)UnitTests",
        shared: true,
        buildAction: .buildAction(targets: [
            .target("\(hostTargetConfig.name)UnitTests"),
        ]),
        testAction: testAction,
        runAction: .runAction(
            configuration: .debug,
            executable: .target(hostTargetConfig.name),
            arguments: .arguments(
                environmentVariables: [
                    :
                ],
                launchArguments: [
                ]
            )
        )
    )

target for IntegrationTests

let result: Target = .target(
    name: "\(hostTargetConfig.name)IntegrationTests",
    destinations: My-ProjectConf.destinations,
    product: .unitTests,
    bundleId: "\(hostTargetConfig.bundleId)IntegrationTests",
    deploymentTargets: My-ProjectConf.deploymentTargets,
    infoPlist: .default,
    sources: [
        "My-Project/Tests/IntegrationTests/Sources/**",
    ],
    resources: [
        "My-Project/Tests/IntegrationTests/Resources/**",
    ],
    dependencies: [
        .target(name: hostTargetConfig.name),
        .xctest,
        .external(name: "Mocker"),
    ],
    settings: Self.testsTargetSettings()
)

Scheme for IntegrationTestsTarget

let testAction: ProjectDescription.TestAction? = .testPlans([
    .relativeToManifest("My-Project/Tests/IntegrationTests.xctestplan")
])

let result: Scheme =
    .scheme(
        name: "\(hostTargetConfig.name)IntegrationTests",
        shared: true,
        buildAction: .buildAction(targets: [
            .target("\(hostTargetConfig.name)IntegrationTests"),
        ]),
        testAction: testAction,
        runAction: .runAction(
            arguments: .arguments(
                environmentVariables: [
                    :
                ],
                launchArguments: [
                ]
            )
        )
    )

Questions:

  1. How can I resolve the error “Scheme “My-LondonApp” is not testable.”?
    how to make “My-LondonApp” testable(according to this, target has to start tests from my test plan “AllProjectTests.xctestplan”)?

  2. What is the correct approach to defining associated test targets

  • unitTests - target “My-LondonAppUnitTests”
  • integrationTests - target “My-LondonAppIntegrationTests”
  • uiTests - target “My-LondonAppUITests”

in Tuist’s manifest to ensure they are correctly related to the main target?

Additional information
this is how I define project

let project = Project(
    name: "My-Project-Tuist",
    organizationName: My-ProjectConfigs.organizationName,
    options: .options(
        disableSynthesizedResourceAccessors: true
    ),
    settings: settingsForProject(),
    targets: projectTargets
    schemes: projectSchemes
    fileHeaderTemplate: "  Created by ___FULLUSERNAME___ on ___DATE___.\n//  ___COPYRIGHT___"
)

what I understand, if I want to use my own TestPlans, I have to create a custom Scheme for a target and provide a Path to testPlans file via .testAction

interesting, it looks like a bug:
if I dont provide “projectSchemes” then call

tuist generate

“My-Project-Tuist” will contain target of app “My-LondonApp” and last created target with “product: .unitTests,”

Hi @Vitalik

To check whether its’ a bug or an issue with the configuration, I’d need from you a reproducible projects with a set of steps.

ok, I will prepare… and notify you

Hello Pedro,

This is the environment where you can reproduce the issue:
testIssue_src.zip (29.5 MB)

Project Structure:
The example includes two projects::

  • “My-Project-Tuist” - Used to build apps
  • “MyApps-Modules-Tuist” Used to manage features/modules

Targets Description:

In “My-Project-Tuist”, I have the following targets:

  • “AAAA” – App target.
  • “My-LondonApp” – App target.
  • “My-LondonAppUnitTests” – Test target (product: .unitTests) for unit tests.
  • “My-LondonAppIntegrationTests” – Test target (product: .unitTests) for integration tests.

Issues:

1 Running Tests in Xcode on “My-LondonApp”
When I attempt to run tests via Xcode for “My-LondonApp” , I encounter the following issue:


it has to run all tests based on “AllProjectTests.xctestplan”.

p/s: I think some issue with testplan.

  1. Missing “My-LondonAppIntegrationTests” When Custom Schemes Are Removed
    If we remove custom schemes from the project definition:
let project = Project(
    name: "My-Project-Tuist",
    targets: projectTargets,
    // schemes: projectSchemes  <- remove this
)

then in Xcode, we can not see target “My-LondonAppIntegrationTests”.
it looks like we can not provide more then one tests target (product: .unitTests,) to one hostTarget

Hi @pepicrft
did you able to rebroduce this issue in test project?

Hi @Vitalik

I haven’t been able to looked into it yet. I’ll let you know once I get the chance.

Hi @pepicrft,

The question is no longer relevant — I’ve identified the issue (it was in my manifest).

Thanks again for Tuist — it opens up a ton of new possibilities for streamlining the development process!

Also, I think it would be great if the .relativeToManifest() method could throw an error or provide some feedback when the specified path can’t be resolved.
For example:

.relativeToManifest("MyApps/App/Tests/TestPlans/ProjectTests.xctestplan")