So we’re working on supporting Swift 6 by enabling full Strict Concurrency Checking. And wow, the warnings swarm in test code. Here’s a screenshot of the top part of one XCTest suite before increasing the warning level:

Looks fine. Then we flip the Strict Concurrency Checking switch, and wow, do the warnings light up!

All the complaints are about main actor isolation. The first says,
Call to main actor-isolated initializer 'init' in a synchronous nonisolated context; this is an error in the Swift 6 language mode
…And so on, for every method, every property. Every assertion that is checking a property says,
Main actor-isolated property cannot be referenced from a nonisolated autoclosure
We were proud of our test code, using XCTest to verify our view controllers. Now Xcode is no longer putting up with it. What’s going on? And what do we do about it?
XCTest and UIViewController Isolation
Let’s slow down and try to understand what’s going on. What is it about an XCTestCase talking to a UIViewController that causes so many warnings?
There’s nothing special about this view controller declaration:
class TrailMapViewController: UIViewControllerAll it does is subclass UIViewController. But if we “Jump to Definition” by command-clicking UIViewController, we can see @MainActor in its declaration:
@MainActor open class UIViewController : UIResponder, NSCoding, UIAppearanceContainer, UITraitEnvironment, UIContentContainer, UIFocusEnvironmentAll UI changes must be on the main thread, so it makes sense to isolate UI elements to the main actor. Strict concurrency checking changes a runtime error (making a UI call from a different thread) into a compilation warning. With Swift 6, this changes from a warning to an error.
So all the warnings about “you can’t call this main actor-isolated thing” refer to the view controller being on @MainActor. It doesn’t just mean it runs on the main thread. It’s an Actor enforcing isolation, ensuring that everything calling it is in the same actor.
The second half of the warnings are about trying to call things from “a synchronous nonisolated context.” It’s referring to our XCTestCase subclass. Let’s break this down.
Synchronous, so not async. Things outside of an actor can still call it if they use an async call. Then the code waits for the actor to say, “I’m preventing data races by taking one caller at a time. Please wait. Okay, now it’s your turn.”
Nonisolated, so not an actor. Let’s fix things by making our test code run inside the main actor.
Change the Tests to @MainActor
In the early days of @MainActor, we could just declare it at the test class level:
@MainActor
final class TrailMapViewControllerTests: XCTestCaseThat worked fine. But try doing this in Xcode 15, and we get this warning instead:
Main actor-isolated class 'TrailMapViewControllerTests' has different actor isolation from nonisolated superclass 'XCTestCase'; this is an error in Swift 6
Command-clicking XCTestCase to jump to its definition shows us something you may not have seen if you’ve only coded in Swift:
@interface XCTestCase : XCTestThis is Objective-C, which knows nothing about actors. For Xcode 15, my fix was to annotate the test methods themselves as @MainActor when necessary:
@MainActor
func test_loadingView_loadsImage()But with Xcode 16, this is no longer necessary! We can once again say the entire test suite runs on the main actor:
@MainActor
final class TrailMapViewControllerTests: XCTestCaseIt’s so much less work.
@MainActor with setUp and tearDown
Whew, most of the warnings disappear! But not all of them…

The warning on setUp is still there, and we’ve introduced a new one on tearDown.
And here we face a challenge. Remember, XCTestCase is old. The setUp and tearDown methods have been around for a long time, happily living in Objective-C with no awareness of actors. The throwing versions don’t help us either.
Use setUp() async throws
But somewhere along the way, Xcode introduced something I must have missed in the release notes. There are async throws versions of setUp/tearDown available in Swift! Let’s see what happens if we use them.
override func setUp() async throws {
try await super.setUp()
sut = TrailMapViewController()
}
override func tearDown() async throws {
sut = nil
try await super.tearDown()
}Note that this means calling the async throws versions in the superclass as well. This may not be strictly necessary when directly subclassing XCTestCase. The implementations in XCTestCase are Template Methods which do nothing. But in object-oriented programming, it’s generally bad form to override a method without calling the superclass. If you break the Template Method design pattern, you will feel the pain if you decide to move some portions of setUp/tearDown to a new base class.
Make the Test Suite Sendable
…Well, we still have warnings. But they’ve changed.

The warnings now say:
Sending main actor-isolated value of type 'XCTestCase' with later accesses to nonisolated context risks causing data races; this is an error in the Swift 6 language mode
Remember, XCTest is a framework that runs our code. There is code, like the test runner, which we don’t control. Old code written in Objective-C is bound to trip up actor isolation.
But the warning starts with the word “sending.” If we mark the class as Sendable, what happens?
final class TrailMapViewControllerTests: XCTestCase, Sendable
Well, I’m surprised. Xcode accepts it, and the warnings disappear! I had assumed that it would require an @unchecked Sendable, but it seems the @MainActor declaration already takes care of most sendable issues for us.
Alternatives
What other options do we have to tame these concurrency warnings? Here are two.
Avoid setUp/tearDown
Strictly speaking, setUp and tearDown aren’t necessary. Roy Osherove writes in The Art of Unit Testing,
My preference is to have each test create its own mocks and stubs by calling helper methods within the test, so that the reader of the test knows exactly what’s going on, without needing to jump from test to setup to understand the full picture.
For example, instead of relying on setUp to create the sut (System Under Test), we can create it within each test method.
func test_loadingView_loadsImage() {
let sut = TrailMapViewController()
sut.loadViewIfNeeded()
XCTAssertNotNil(sut.imageView)
}But this does spread duplication throughout our tests. As soon as we add a parameter to the initializer, we need to change it everywhere, which is a pain. It’s better to create a helper function. And being an ordinary function of our making, it inherits the class-level declaration that everything will run on the main actor.
private func makeSUT() -> TrailMapViewController {
TrailMapViewController()
}
func test_loadingView_loadsImage() {
let sut = makeSUT()
sut.loadViewIfNeeded()
XCTAssertNotNil(sut.imageView)
}Now if we need to add an argument, we can do it in one place. And a helper gives tests more flexibility to specify how to set things up.
Convert to Swift Testing
Swift Testing avoids interplay with Objective-C, so it’s fine living in a world of actors. One option to fix these concurrency warnings is to jump ahead and convert your tests from XCTest to Swift Testing. The Claude LLM is helpful for this.
But I would still exercise caution, and recommend doing that as a follow-on step. I live by a motto, “Separate the problems.” We are first trying to appease Xcode’s warnings so that we can gradually migrate to Swift 6. I have described above the smallest change that gets us there.
Also, there are a few tests that aren’t yet ready for Swift Testing. I’ll describe these in a separate article.
Conclusion
Remember that we don’t need to fix every warning in a single commit or a single work session. I recommend this incremental workflow:
- Decide on a timebox, and set a timer.
- Temporarily change Strict Concurrency Checking to “Complete”.
- When you finish a single file (or when your time is up), commit. Remember not to include the change to Strict Concurrency Checking in your commit.
- When your time is up, set Strict Concurrency Checking back to “Minimal”.
- Push your changes and pat yourself on the back. You’ve made your code a better place.
Changing the Strict Concurrency Checking level is simpler if you use XcodeWarnings. Scroll to the bottom for the Swift Compiler features. For more info, see Xcode Warnings: Turn Them Up to Eleven!
To make it easier to write new XCTest suites that follow this approach, I’ve updated both my file templates and code snippets. Less thinking, more flow.
For more on working through other changes for Swift 6, see A Conversation With Swift 6 About Data Race Safety.
Thanks to the participants of Overridden methods can't have different actor isolation from nonisolated overridden declaration for raising this Swift issue and offering the solutions.
If you found this article helpful, please share it with your colleagues.

Thanks for that Sendable @MainActor, couldn’t find that solution anywhere else.
Glad I could help, Alejandro!
Thanks a lot for this, my initial thought was to make test classes isolated to MainActor but for the love of me couldn’t figure out setUp and tearDown, I too wasn’t aware of those async variants. 😮
I have one more observation, I believe sendable conformance is not needed anymore, at least on Xcode 26.2 with approachable concurrency and nonisolated by default. I use XCTest only for UITests though.
Thanks Markus, I haven’t yet tried approachable concurrency. I assume that means more strict annotations aren’t needed, but are allowed, so my templates will continue to work even with approachable concurrency.
I use XCTest for:
– approval tests, because ApprovalTests.swift still requires it (I’ll add Swift Testing support some day)
– SwiftUI tests, because ViewInspector still requires it