Using Firebase Authentication in Tests with the Firebase Emulators
Firebase is a platform to build apps without managing infrastructure. To support testing Firebase offers Firebase emulators to test the functionality locally. On our own machine. Without the need to connect to the real Firebase services.
Using the emulator makes our tests run a lot faster. Something which is paramount when doing Test Driven Development.
The emulator works fine for Cloud Firestore, their database offering, and Cloud Functions, which allows to run backend functions without managing servers.
Not yet for Firebase Authentication though.
Firebase Authentication, for example, allows authentication via phone number.
We enter a phone number, get a verification code via SMS, enter the verification code and are authenticated.
Let’s say we want to find the
uid of a user by phone number in one of our services for some reason. We can use Firebase Auth.
But if we have the code to get the user by its phone number called during the emulator tests using
It yields an error about our missing Firebase credentials.
Error: Credential implementation provided to initializeApp() via the "credential" property failed to fetch a valid Google OAuth2 access token with the following error: "Error fetching access token: Error while making request: getaddrinfo ENOTFOUND metadata.google.internal metadata.google.internal:80. Error code: ENOTFOUND".
Of course, we did not set them up, we were attempting to run against an emulator on our own machine.
We would need to set up a connection to the real services as described in the official docs.
Ok. Seems we need to have some valid credentials during testing then?
Not necessarily. We can take a different approach. Let’s first wrap the call to using the
In our service using the functionality, we replace the direct call, using
firebase-admin, with the wrapping function above
Which allows us to replace the module
getUserByPhonenumberwith a mock during testing. A mock would be an implementation that acts as a stand-in for the actual function during testing. For example an implementation that always returns the same phone number without ever accessing the real Firebase or the emulator.
This is possible because whatever service wants to use the phone number does not care how
getUserByPhonenumber works under the hood. The service expects that the real implementation of
getUserByPhonenumber is tested in the tests for the module.
Using the mocking library testdouble.js we create the following setup in our tests.
Important to remember is the
require of our service under test, in our case
service.js, needs to happen after testdouble has replaced the real implementation of
getUserWithPhonenumber. The code in line 6 above needs to be after the code in line 5.
If we switched the order then
service would get the real implementation making us wonder why all our mocking does not happen.
In our tests we can now tell the mock what to return when it is called.
Thus when our service under test will call
getUserWithPhonenumber with exactly the phone number
"123123123123" he will get the stubbed
existingUser. No Firebase auth will be involved.
One might wonder why we are not mocking the Firebase Auth module directly. Why create the abstraction? It boils down to the mantra
Don’t Mock What You Don’t Own
We don’t own the third party dependency
firebase-admin . We have no direct influence on its design choices. They might decide to change their API in the future, which would force us to change the behaviour of our mock. What is now
getUserByPhoneNumber might in a future version become
getAccountByPhone taking different arguments and return a different object. We don’t know yet.
There is an additional benefit of wrapping the call to Firebase Auth besides testing. Wrapping the function makes it easier to replace the functionality if we were to replace Firebase Auth with something different later and by wrapping all the Firebase Auth functionality we are using in such a service we create one single spot where we can see which functions of Firebase Auth are in use. Giving us an overview on what functionality the replacement would need to provide or what we would have to implement ourselves.
The question arises if the real
admin.auth.getUserByPhoneNumber is ever tested? Yes it should be. But not in the service where we are testing business logic.
We can always create an end to end test which would use a real login and then does something a user would do in our application. For example log in in, search for a product and then buying it.
We could set this up with tools such as cypress.