Test driving authentication flow in a public-to-read / private-to-write application.
You may have developed, or might be developing right now, an app that needs users to be authenticated to generate content. But from a user perspective, when the login screen is the first thing thrown at your face, it feels kind of forceful.
Why not let users goof around, see whatever content there may be, and if they like it and feel like creating something themselves, then we ask them to login. By that time the user knows what the app is about, is hooked up and won’t mind authenticating.
So we will create an app which contents are public to read but private to write. That means you don’t need to login right away, you can browse through the content, but you cannot create it unless you login.
We will use Kotlin to write the app, Firebase for authentication, Android Architecture Component’s ViewModel as our lifecycle aware object, and Koin as dependency injector. Also, for the testing part, we will use Mockito and Kotlintest.
If you just want to see the final result, take a look at the code.
In this sample app there are 3 screens:
Main screen, showing available content (Public, no authentication required)
New content screen, allowing to create new content (Private, authentication required)
We will treat our Activities as our View layer, that means no business decisions will be made here, just presentation details. We will delegate business decisions to our ViewModel.
The user sees public content, but authenticates to generate some. Once authenticated, navigation is seamless throughout the app.
There is one behavior we want to test: when the user presses the “create content” button, we check whether the user is logged in or not. If logged in, the user goes to the “content creation” screen. If not, he ends up in the “login” screen.
Let’s take a rough first approach for this. Remember the code is Kotlin, but fear not, it is very similar to Java and you can even convert it just by copy-pasting code in a .java file.
Well, that works. But there are a few problems here.
The first one is conceptual: we do not want the views to make business decisions, and the Activity IS the view. In fact, the activity here does authentication, business logic decisions and view presentation.
Secondly, we have tied a core business decision to an external library (FirebaseAuth).
Finally, we want to be able to unit-test this rule, but we cannot mock the dependencies of the activity since they are being injected and we do not have access to the activity’s constructor (you actually can, but it is more complicated and not useful here). So let’s refactor this a bit.
We have created an Authenticator that wraps the FirebaseAuth library, thus creating a clear boundary between our app and externals, which is always good to keep architecture clean. Now we could easily mock that authenticator to create the specific conditions we want to test: the user is authenticated, or not. But with things like this, we would have to assert if the resulting Intent has some internal specific information. This would mean tying our tests to the Firebase AuthUI library (by checking for data this library might put inside the Intent) and therefore breaking the boundary we’ve just built.
Instead of that, we can delegate the Intent creation to a collaborator. You could use several approaches here. We kept it simple with a single class responsible for creating both intents we need: IntentGenerator. We also created AuthUIWrapper to draw another boundary, pushing this detail (the use of Firebase AuthUI library) away from our core code.
Having things like this allows us to use the ViewModel as a test subject and spy on the IntentGenerator. We can then stub the Authenticator so we can set the authentication status we want, and check which intent was indeed called by the ViewModel.
If the user is authenticated, ViewModel should have requested the content creation intent. If not, the authentication intent should have been requested instead. Finally, the intent is delivered to the MainActivity, and a new Activity is created based on the obtained intent. Once the user is authenticated, he will navigate the app seamlessly.
In the end we have a very cohesive set of classes (MainActivity, AuthViewModel, Authenticator, IntentGenerator), each with clear responsibilities, which makes for a more extensible, easier to test, cleaner architecture.
One more thing, to speed things up here we used the Firebase authentication activity, but with this approach you can easily switch later to your own login / signup activity by returning your login intent instead of the AuthUI one, in the IntentGenerator.