Capturing values in Swift Testing in strict concurrency mode

I’ve recently turned on Swift 6.0 mode on of my projects and that means migrating all code to strict concurrency. I had some tests that used mocks to capture values and then using an #expect macro to check whether the captured values were the correct ones. One such test looked like this:

@Test
func loginWithNoAccountsAndSuccessfulLoginSavesTokenInCredentialManager() async throws
{
    let expectedCredentials = AccountCredentials(account: "test@instance.social",server: "instance.social", token: "mynewaccesstoken")
    
    var instanceUrlCalled: URL?
    var storedCredentials: AccountCredentials?
    let appModel = withDependencies {
        $0.defaultDatabase = try! appDatabase()
        $0.socialMediaClient = MockSocialMediaServer(connectToInstance:  { url in
            instanceUrlCalled = url
            return expectedCredentials
        })
        $0.accountCredentialsStorage.storeCredentials = { credentials in storedCredentials = credentials
        }
    } operation: {
        AppModel()
    }
    
    #expect(appModel.destination == nil)
    
    try await appModel.login()
    
    guard let expectedUrl = URL(string: "https://instance.social") else {
        Issue.record("Failed to creater URL from string")
        return
    }
    
    #expect(appModel.destination.is(\.onboardingScreen))
    #expect(expectedUrl == instanceUrlCalled)
    #expect(expectedCredentials == storedCredentials)
}

However, capturing these variables caused problems in strict concurrency mode. I could lead to potential data races and therefore swift 6.0 does not allow it. It took me some time to figure out how to fix this, and it turned out you shouldn’t capture any values at all! The fix was as easy as putting the #expect macros in the closures passed to the mock, like this:

@Test
func loginWithNoAccountsAndSuccessfulLoginSavesTokenInCredentialManager() async throws
{
    let expectedCredentials = AccountCredentials(account: "test@instance.social",server: "instance.social", token: "mynewaccesstoken")
    
    guard let expectedUrl = URL(string: "https://instance.social") else {
        Issue.record("Failed to creater URL from string")
        return
    }

    let appModel = withDependencies {
        $0.defaultDatabase = try! appDatabase()
        $0.socialMediaClient = MockSocialMediaServer(connectToInstance:  { url in
            #expect(expectedUrl == url)
            return expectedCredentials
        })
        $0.accountCredentialsStorage.storeCredentials = { credentials in
            #expect(expectedCredentials == credentials)
        }
    } operation: {
        AppModel()
    }
    
    #expect(appModel.destination == nil)
    
    try await appModel.login()
    
    #expect(appModel.destination.is(\.onboardingScreen))
}

Oh, and by the way, if you’re expecting something not to happen, the test should look like this:

@Test
func loginWithNoAccountsInDatabaseSetsDestinationOnboarding() async throws
{
    try await confirmation(expectedCount: 0) { @MainActor confirmation in
        
        let appModel = withDependencies {
            $0.defaultDatabase = try! appDatabase()
            $0.socialMediaClient = MockSocialMediaServer(connectToInstance: {
                url in confirmation()
                return AccountCredentials(account: "",server: "", token: "")
            }, connectToInstanceWithToken: { url, token in confirmation() })
            $0.accountCredentialsStorage.retrieveCredentials = { _ in
                confirmation()
                return ""
            }
            $0.accountCredentialsStorage.storeCredentials = { _ in  confirmation() }
            $0.accountCredentialsStorage.updateCredentials = { _ in  confirmation() }
            
        } operation: {
            AppModel()
        }
        
        #expect(appModel.destination == nil)
        
        try await appModel.login()
        
        #expect(appModel.destination.is(\.onboardingScreen))
    }
}

Just use a confirmation with expectedCount equal to zero. And call the confirmation() function in each closure you don’t expect to be called. Hope that helps! See you next time 😃