Sign-In With Apple in TCA — Part 3

How do we test this thing?

Sam McGarry
4 min readJan 8, 2023

Recap and testing approach

We’ve now built this cool little app that allows us to sign-in with Apple.

However, you may be feeling like we went on a bit of detour to get things working with TCA. We certainly could have achieved the same thing with much less code in vanilla SwiftUI.

We’ve also created several custom types (such as the AppleAuthenticationResponse type) to carry information that is otherwise carried by internal types from the AuthenticationServices framework.

So why did we go through the hassle of abstracting everything with TCA?

Well, with TCA we have the power to easily write tests for practically any logic we’ve defined in our app. As we are using custom types, we will easily be able to mock interactions with dependencies. We also won’t have to worry about our views at all, since that logic is almost entirely decoupled from the UI.

Testing Method

In regular iOS unit tests, we would assert changes using XCTAssert. To test TCA code we’ll have to useTestStore, an object that can help us mock behavior in our TCA features.

We’ll be using the send() function to trigger actions of our choosing, and
the receive() function to verify other TCA actions are received, as well as verify any changes that occur to the app’s state.

Root TestStore

In all of the tests we’ll be writing, we’ll be using the Root state/reducer, as it is the feature that manages app level state changes.

let store = TestStore(
initialState: Root.State(),
reducer: Root()
)

What should we test?

If we break our app down into events that trigger changes in our app’s state (e.g. behaviors), there are three such events that come to mind;

  • Successfully signing in with Apple via the SIWA button
  • Signing out of the app via the “Sign out” button
  • Checking the user’s authentication status on launch

Let’s write some tests

Successful Sign In Test

Since our UI is state-driven, we can test a successful sign-in by verifying that the app’s state changes to the Main state after the SIWA button is tapped.

Event Trigger

  • First, we can create a mock AppleAuthenticationResponse (as one is generated when the user signs in).
  • Then to replicate what happens with a successful sign in, we can use store.send() to send the .signInWithAppleButtonTapped action with our mock response.

Verification

  • The store should receive then .didSignIn delegate action from the SignIn feature. We can verify that using TCA’s store.receive().
  • The app’s state should then change to .main(Main.State()). We can verify this in the closure that is passed into the store.receive() call.
$0 = .main(Main.State())

Sign Out Test

To test signing out, we’ll want to check that the app’s state changes to the SignIn state, as the app should return to the sign-in view.

Event Trigger

  • To replicate what happens after a sign-out occurs, we can send the .signOutButtonTapped action.

Verification

  • The store should then receive the .didSignOut delegate action.
  • The app’s state should then change to .signIn(SignIn.State()).

Authentication Status Check Tests

The tests we write for verifying our authentication check works will differ a bit from the tests we’ve already written, as the authentication check relies on the AuthenticationClient dependency.

This means we’ll need to use the unimplemented version of the AuthenticationClient (that we set up in part 1 of the article), which will allow us to mock responses from the client.

After setting up a mock, we’ll override the TestStore’s authenticationClient directly with our unimplemented client via the dependencies property.

var authenticationClient = AuthenticationClient.unimplemented

authenticationClient.checkAppleAuthenticationStatus = { _ in
.signedIn
}

store.dependencies.authenticationClient = authenticationClient

User is signed in on launch

To test how the app behaves when the user is already signed in on launch, we can mock a .signedIn status being returned from the authentication check.

Event Trigger

  • Firstly, we’ll need to pass the .signedIn status into the checkAppleAuthenticationStatus property of our unimplemented client.
  • Then we can send the ._onAppear action, as that kicks off the authentication check (as we set it up in part 2 of the article).

Verification

  • The store should then receive the ._checkAuthenticationStatusResponse action with the .signedIn status we passed into the unimplemented client.
  • The app’s state should then change to .main(Main.State()).

User is signed out on launch

To test the the scenario where the user is signed out on launch, we can almost reuse the test we just wrote, with only a few small changes.

Event Trigger

  • This time, we pass the .signedOut status into the checkAppleAuthenticationStatus property.
  • Then we can send the ._onAppear action again to kick off the authentication check.

Verification

  • The store should then receive the ._checkAuthenticationStatusResponse action with the .signedOut status this time.
  • And there should be no change in state, as the default state on launch is the SignIn state — so no change would be detected (even when the SignIn state is assigned).

Conclusion

With that — we’ve covered the set up, UI, and a testing approach for implementing Sign-In With Apple using TCA. If you found this article useful or helpful at all, please reach out on Twitter or Mastodon. Have a good day!

--

--