Angular, Firebase, and TDD [Part 1]— Setup the app and create an authentication service

Aysha Williams
Aysha’s Handmade Code
8 min readJan 11, 2021

I’m spinning up a new project using Angular, Firebase, and Angular Material. I’ll be using TDD to develop this web app, and since it’s all greenfield right now, I’m going to document the process.

Getting Started

Angular, Firebase, and Angular Material are great tools to use for rapid application development. This post will focus on getting Angular and Firebase setup.

  1. Spin up a new Angular project ng new budget-tool
  2. Set up a new project in Google Firebase.
    a. Read these instructions on how to do so.
  3. Add Cloud Firestore to the Google Firebase Project.
    a. Read these instructions on how to do so.
  4. Add AngularFire to the Angular project (AngularFire is the api for Firebase)ng add @angular/fire
  5. Add the project’s Firebase configuration to the /src/environments/environment.ts file by following these instructions. Note: The database url follows the pattern https://<PROJECT_ID>.firebaseio.com
  6. Run ng test. All of the tests should be green, and they will keep running while ng test runs.

Add account creation

Firebase and AngularFire make adding an authentication service fairly easy.

Use the angular cli to generate a service class and a spec file.

ng g service services/authentication

Two files will be created: authentication.service.spec.ts and authentication.service.ts . The spec file will have a single test inside of it.

This test and all of the other tests should be passing.

Add a test to create an account

This test will try to create an account.

it('creates an account', () => {
const service = TestBed.get(AuthenticationService);
const result = service.createAccount("test@test.example", "password");

expect(result).toEqual(true);
});

Since the createAccount function doesn’t exist, that will be the first test failure.

TypeError: service.createAccount is not a function

Fix the test.

The simplest solution is to create a function named createAccount and return true. So, that will be first change to authentication.service.ts.

createAccount(email:string, password:string) {
return true;
}

The test is green.

Use Firebase to create an account.

Now, it’s time to hookup createAccount to the Firebase authentication api.

The app can interact with Firebase using the AngularFire library. There’s a built-in function called createUserWithEmailAndPassword that will be used for account creation.

To use the createUserWithEmailAndPassword function, the AuthenticationService needs a reference to the AngularFireAuth class which will be done using Angular’s dependency injection. The AngularFireAuth class will be injected into the constructor of the AuthenticationService. Then, the createAccount function will be turned into an asynchronous function, since the createUserWithEmailAndPassword function returns a promise, and the await operator is going to be used to manage the result that is returned.

This is the updated createAccount function and the AuthenticationService service. In bold, are the new additions.

import { Injectable } from '@angular/core';
import { AngularFireAuth } from '@angular/fire/auth';
@Injectable({
providedIn: 'root'
})
export class AuthenticationService {
constructor(public afAuth: AngularFireAuth){}
createAccount(email:string, password:string) {
const result = this.afAuth.createUserWithEmailAndPassword(email, password);
return true;
}
}

Now, the tests are failing.

Fix the tests.

The Angular specs aren’t bootstrapped with all of the imports and providers that are used in app.module.ts, so the tests are failing because it needs to add AngularFireAuth as a provider and it also needs to initialize the Firebase app by running AngularFireModule.initializeApp as part of the TestBed imports.

Below, the TestBed has been updated to configure a provider for AngularFireAuth and initialize AngularFireModule. In bold, are the changes.

beforeEach(() => {
TestBed.configureTestingModule({
imports: [
AngularFireModule.initializeApp(environment.firebase),
],
providers: [
AngularFireAuth,
]

});
service = TestBed.inject(AuthenticationService);
});

Note:AngularFireAuth, AngularFireModule, and environment are imported above. Also, notice that the tests is initialized with what could be production configuration. In a later post, it will be configured with a development configuration.

And, voila! The tests are green again!

Executed 5 of 5 SUCCESS (0.321 secs / 0.251 secs)

Fix the console error.

But wait, there’s an error in the console.

'Unhandled Promise rejection:', '{"error" {"code":400,"message":"CONFIGURATION_NOT_FOUND" ...

This is undoubtedly due to the configuration in environment. So, the implementation needs to be adjusted to return the result from createUserWithEmailAndPassword, and it will need to catch any errors that occur. The test will also need to be updated since it will be using await to handle the result from createUserWithEmailAndPassword.

Changes to authentication.service.ts are in bold, below.

async createAccount(email:string, password:string) {
try {
const result = await
this.afAuth.createUserWithEmailAndPassword(email, password);
return result;
} catch (e) {
return false;
}

}

Changes to authentication.service.spec.ts are in bold, below.

it('creates an account', async () => {
const result =
await service.createAccount("test@test.example", "password");

expect(result).toEqual(true);
});

Now the test is failing, but due to a different issue.

Error: Expected false to equal true.

Fix the CONFIGURATION_NOT_FOUND error

To fix the error, the Sign In method needs to be configured from within the Firebase console.

After logging into the Firebase console, go to the Authentication page. When the page loads for the first time, a Getting Started button will appear. After clicking on the Getting Started button, the authentication settings page will be displayed.

The list of providers is under the Sign-in Method tab/link.

All of the authentication providers are turned off by default. In order to use the createUserWithEmailAndPassword function, the Email/Password provider needs to be enabled.

The beauty of Firebase is that it comes with a lot of pre-built functionality, such as these authentication providers. Authentication methods can’t be used from code, unless their enabled from the console. In addition to this security guard, Firebase also provides password recovery, email address verification, and a few more features are included as part of the platform.

Fix the test some more

Now the test is failing because the value being returned from createAccount is not a boolean but an object representing the new user who is now signed in.

Error: Expected Object...to equal true

This error can be easily fixed by turning the object into a boolean using a double bang !!.

Now, the createAccount function looks like so: (updates are in bold)

async createAccount(email:string, password:string) {
try {
const result = await
this.afAuth.createUserWithEmailAndPassword(email, password);
return !!result;
} catch (e) {
return false;
}
}

Stabilize the test.

Now, the only problem with the implementation is that a live account is being created in the firebase app. So, on every subsequent run of the test, it will fail because the same email address is being used to create an account.

This implementation of the test makes it fickle, but it can be easily resolved by mocking out the createUserWithEmailAndPassword function. But, for now, the tests will remain as an integration test. In the future, the app will be configured to use an emulator, instead of hitting the production level platform. This is best practice and much better than junking up Firebase with test emails.

Since this test will remain as an integration test, the emails need to be created uniquely when the test runs. Luckily, Google found a gist on github which contains a code snippet that can be used to randomize a string. This particular example attempts to generate a UUID, but the level of randomization will still be useful for this test.

The new test is below. The changes are in bold.

it('creates an account', async () => {
const randomString = () => Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
const email = `${randomString()}@${randomString()}.test`;
const password = randomString();
const result = await service.createAccount(email, password);
expect(result).toEqual(true);
});

Now, this test can be ran over and over again without failing due to duplicate account creations.

If I check the authentication page in the Firebase console, there will be a number of randomly generated emails. These all can be deleted from the console. Although, they can only be deleted one at a time.

Add Login abilities

Logging in will also be just as easy as creating an account, but first, a test is needed.

This test will duplicate code from the previous test to create an account (don’t worry a refactor is in the near future), then check if a login is successful by expecting true to be returned from the login function.

This is the new test for the AuthenticationService.

it('logs users in', async() => {
const randomString = () => Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
const email = `${randomString()}@${randomString()}.test`;
const password = randomString();
const result = await service.createAccount(email, password);
expect(result).toEqual(true);
const loginResult = service.login(email, password);
expect(loginResult).toEqual(true);
});

It fails, expectedly, because login doesn’t exist on the AuthenticationService. So that’s the first change to be made. The second will be to return true from that function (similar to the steps taken with createAccount).

This is the login function for the AuthenticationService.

login(email: string, password:string) {
return true;
}

Tests are green again.

Use the Firebase api to login

There’s a login function called signInWithEmailAndPassword it takes two strings, email and password to log a user into an account.

Changes to the login function are below, in bold.

login(email: string, password:string) {
this.afAuth.signInWithEmailAndPassword(email, password)
return true;
}

Handle invalid emails and passwords

Simply adding a call to signInWithEmailAndPassword keeps the tests passing because the return type is hardcoded as true. Now a test will be added for the case when logging in should return false, such as when an invalid email and password are submitted.

See the new test below.

it('does not log users in when the email and password are invalid', () => {
const loginResult = service.login('invalid@email.invalid', 'password');
expect(loginResult).toEqual(false);
});

So now the tests fail with this error:

Error: Expected true to equal false.

The signInWithEmailAndPassword returns a Promise which can be used to determine the success or failure of a login attempt. So, the login function is turned into an async function, it uses await to wait for the result returned from signInWithEmailAndPassword , then it converts the result into a boolean.

async login(email: string, password:string) {
const result = await this.afAuth.signInWithEmailAndPassword(email, password);
return !!result;
}

Now the tests need to be updated to handle the async function. Changes for the test are below in bold.

it('logs users in', async () => {
const randomString = () => Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
const email = `${randomString()}@${randomString()}.test`;
const password = randomString();
const result = await service.createAccount(email, password);
expect(result).toEqual(true);
const loginResult = await service.login(email, password);
expect(loginResult).toEqual(true);
});
it('does not log users in when the email and password are invalid', async () => {
const loginResult = await service.login('invalid@email.invalid', 'password');
expect(loginResult).toEqual(false);
});

Now the tests fail, but for a different reason.

Error: There is no user record corresponding to this identifier. The user may have been deleted.

This is an exception, and it can be caught in a try-catch within the login function. If an exception is thrown, then the user is not logged in and the login function will return false.

Changes to the login function are below.

async login(email: string, password:string) {
try {
const result = await this.afAuth.signInWithEmailAndPassword(email, password);
return !!result;
} catch (e) {
return false;
}
}

And voila! All tests are green!

Refactoring

Since the tests for creating an account and logging in use the same code, those chunks of code can be extracted into a function. The email and password generation is extracted into its own function within the test. The new function will return a tuple containing the generated email and password.

The new function is below:

const generateEmailAndPassword = () => {
const randomString = () => Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
const email = `${randomString()}@${randomString()}.test`;
const password = randomString();
return { email, password }
}

The tests will initialize their own email and password variables using deconstruction.

Updates to the tests are below.

it('creates an account', async () => {
const {email, password} = generateEmailAndPassword();
const result = await service.createAccount(email, password);
expect(result).toEqual(true);
});
it('logs users in', async () => {
const {email, password} = generateEmailAndPassword();
const result = await service.createAccount(email, password);
expect(result).toEqual(true);
const loginResult = await service.login(email, password);
expect(loginResult).toEqual(true);
});

Final Result

The completed code for this post can be found on StackBlitz. Enjoy!

--

--