Sign-In With Apple in TCA — Part 3
How do we test this thing?
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’sstore.receive()
. - The app’s state should then change to
.main(Main.State())
. We can verify this in the closure that is passed into thestore.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 thecheckAppleAuthenticationStatus
property of ourunimplemented
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 theunimplemented
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 thecheckAppleAuthenticationStatus
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!