How can we bypass the normal SwiftUI app launch sequence during testing?
For apps with an application delegate, I’ve written How To Switch Your iOS App Delegate for Improved Testing. This lets us set up a separate launch sequence for test runs that does only the bare minimum. Can we do the same thing for SwiftUI apps that use @main? Yes, we can.
[This post is part of the TDD with SwiftUI series.]
Why Bypass Normal SwiftUI App Launch?
What does an application do at launch that we don’t want it to do before running tests? In the iOS Unit Testing by Example chapter on “Take Control of Application Launch” I describe things that apps often do upon launch:
- Load persistent data.
- Send an app-specific key to an analytics service.
- Send a request to fetch data it needs before rendering the first screen.
Unless we’re careful, things like these will happen before tests run. During testing, we want to avoid anything that’s slow, loads actual persistent data, or makes any network calls. We also get unrealistic code coverage on these activities and on the initial screen.
The Default SwiftUI Main Entry Point
In non-SwiftUI app launch, I set up a separate main entry point, then use NSClassFromString to dynamically search for a TestingAppDelegate. We use it if it’s available. This works because the TestingAppDelegate isn’t present in the app target. Instead, it lives inside the test target.
That approach doesn’t work for SwiftUI which has a different lifecycle and uses structs instead of NSObject-based classes.
Let’s assume the app is called Production. When Xcode sets up a new project with a SwiftUI interface, it defines ProductionApp as the main entry point:
@main
struct ProductionApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}What do we do with this? There’s a simple solution that will work for simple apps, and a more thorough solution that will work for all apps.
The Simple Solution
In the comments below, David offers a simple solution that will work for your app as long as it doesn’t have custom start-up code. The thorough solution involves creating a MainEntryPoint, a ProductionApp, and a TestApp. David’s simple solution elegantly avoids these without adding any new types:
@main
struct ProductionApp: App {
var body: some Scene {
WindowGroup {
if isProduction {
ContentView()
}
}
}
private var isProduction: Bool {
NSClassFromString("XCTestCase") == nil
}
}In Kent Beck’s Four Rules of Simple Design, the last rule is “Fewest Elements.” On that front, this solution is superior. It alters the main entry point only slightly, wrapping the ContentView in a conditional that checks isProduction. And there are no new types.
Basically, this conditional says, "Let’s make sure we’re running the production app. If we are, great, show the content. If not, leave the view hierarchy empty.”
Note that the conditional works even if you don’t import XCTest.
The Thorough Solution
But what if you have custom start-up code? That’s when you use this thorough solution.
When I first looked at this problem, I felt stuck until I found Jay Lyerly's article Better App Launching with SwiftUI for Unit Tests. I took what I learned from Jay and broke it into small steps.
Introduce a Separate Main Entry Point for SwiftUI
By default, the @main attribute used to identify the main entry point pairs nicely with the App protocol.
But that’s not the only way @main can work. It will run any static func main() it can find. So first, let’s remove @main from ProductionApp. Then create another struct named MainEntryPoint:
@main
struct MainEntryPoint {
static func main() {
ProductionApp.main()
}
}This defines a main() function that calls ProductionApp.main(). We never declared this method ourselves, but it exists. An extension to the App protocol defines a default implementation.
Run the app to confirm that it still launches normally. Right now, the app launch sequence has the same behavior as before, showing the WindowGroup in ProductionApp.
For Testing, Use a Separate SwiftUI App
Now we have an explicit main entry point that we can control. We can still use NSClassFromString to do a dynamic search for an NSObject-based class. But for SwiftUI, let’s search for XCTestCase to check if a test target was injected into the app.
Add a guard clause before ProductionApp.main(), along with a static function to check if we're in production or in test. The whole thing looks like this:
@main
struct MainEntryPoint {
static func main() {
guard isProduction() else {
TestApp.main()
return
}
ProductionApp.main()
}
private static func isProduction() -> Bool {
return NSClassFromString("XCTestCase") == nil
}
}Basically, this guard clause says, "Let’s make sure we’re running the production app. If not, run TestApp.main() instead and get out.”
Now we have to define TestApp:
struct TestApp: App {
var body: some Scene {
WindowGroup {
}
}
}This declares a body with a WindowGroup consisting of nothing.
Unfortunately, to keep the Swift compiler happy, we have to put this TestApp into the main app target. We normally want to keep test code out of production code, but let’s shrug and make this an exception to the rule.
Confirm Normal Launch vs. Testing Launch
Finally, let’s confirm that we have two different app launches. Add this print statement as the first line of the WindowGroup of ProductionApp:
let _ = print("normal launch")And in the WindowGroup of TestApp, add a different print statement:
let _ = print("test launch")Select View > Debug Area > Activate Console from the Xcode menu or press Shift-⌘C. When you run the app, you should see “normal launch” in the console output. And when you run tests, you should be able to find “test launch” before the tests run.
Once you’ve confirmed the two different app launch paths, delete the print statements.
Adding print statements and checking manually… is that test-driven development (TDD)? Partly yes, partly no. I’d say it’s in the spirit of TDD: taking small, verified steps. But that verification isn’t automated. Any manual checking is ephemeral.
Usually, print-statement testing is an opportunity to ask: can we change this design to support automated testability?
But this is at the main entry point, the earliest edge of app execution. So I think it’s not worth reworking for automated testing.
Your Turn
Now it’s your turn to try this on your own SwiftUI app. Each chapter of iOS Unit Testing by Example has activities to try. In that same spirit, try this:
- Enable code coverage and run your unit tests. Look at the reported coverage on your @main app and the initial view.
- Apply either technique above, depending on your code’s start-up needs.
- Run your unit tests again. How does the code coverage look now on your initial view?
How did it go for you? Share your experiences and questions in the comments below.
[This post is part of the TDD with SwiftUI series.]

Thanks for this Jon. This code with two different App implementations seems to have extra convolution for little immediate benefit over a simple if statement:
private var isProduction: Bool {
NSClassFromString(“XCTestCase”) == nil
}
var body: some Scene {
WindowGroup {
if isProduction {
ContentView()
}
}
}
What is the benefit to the extra code in your example? I can see some future use case where you might want to do special test setup at which point it might be relevant, but until you get there, why violate YAGNI?
Because… I didn’t think to? Wow, you’ve found a much simpler solution that will work for most situations!
Daniel, I’ve rewritten my blog post to showcase your solution. It works great. Thank you!
I came up with the original two app scheme. I really like the simplicity of your solution, but for me, it doesn’t work as well in a real app. My production App struct creates some objects that hold global state and are used application-wide. (They’re passed via dependency injection to other parts of the system.) By using a brain dead simple TestingApp, I can be sure that none of those objects are being created. I could do that in the single App scenario with copious use of `isProduction()`, but that opens the door for more mistakes on my part. With the two app scheme, changes in the production app can’t influence _all_ the other tests.
Another perk I like with the two app approach — I can put TestApp and MainEntryPoint in a separate file that is virtually identical across projects and never has to be touched. Then there is zero testing code is my production App struct, so things are cleanly separated.
Jay, now I have to thank you more than once: First, for writing the original article. Second, for explaining the nuance of why you prefer your solution in your cases.
I’ve tweaked my write-up, calling them “the simple solution” and “the thorough solution.”
Thanks! Nice to see other people are coming up with solutions for this as well.
Yeah, I’m glad to see people caring about improving the testing experience for SwiftUI.
I have wanted to separate my production and test apps for some time now. Both solutions looks great!
Oh good!