Test Driven Development with Protractor/Jasmine (Legacy)
This module is deprecated and no longer receives updates. Protractor is likely being removed as the default from Angular applications and Protractor itself will likely stop receiving updates and development in the future. I would recommend checking out the Test Driven Development with Cypress/Jest as a replacement.
Testing Storage and Reauthentication
WARNING: This module is deprecated and no longer receives updates. Protractor is likely being removed as the default from Angular applications and Protractor itself will likely stop receiving updates and development in the future. I would recommend checking out the Test Driven Development with Cypress/Jest as a replacement.
Dealing with complicated scenarios in E2E tests and more unit tests
DEPRECATEDModule Outline
- Resources PRO
- Lesson 1: Introduction PRO
- Lesson 2: Introduction to Test Driven Development PRO
- Lesson 3: Testing Concepts PRO
- Lesson 4: Jasmine, Karma, and Protractor PRO
- Lesson 5: A Simple Unit Test PRO
- Lesson 6: A Simple E2E Test PRO
- Lesson 7: Introduction to Angular's TestBed PRO
- Lesson 8: Setting up Tests PRO
- Lesson 9: Test Development Cycle PRO
- Lesson 10: Getting Ready PRO
- Lesson 11: The First Tests PRO
- Lesson 12: Injected Dependencies & Spying on Function Calls PRO
- Lesson 13: Building out Core Functionality PRO
- Lesson 14: Testing Asynchronous Code PRO
- Lesson 15: Creating a Mock Backend PRO
- Lesson 16: Setting up the Server PRO
- Lesson 17: Testing Integration with a Server PRO
- Lesson 18: Testing Storage and Reauthentication PRO
- Lesson 19: Refactoring with Confidence PRO
- Lesson 20: Conclusion PRO
Lesson Outline
Testing Storage
We have one last E2E test to deal with before all of our initial requirements for the application are finished:
- On subsequent visits to the application, the user should be taken directly to the logged in view
This requirement will give us a chance to write some tests for interacting with Ionic's Storage API, and it will also present some interesting challenges for our existing E2E tests.
Create the E2E Test
Let's start by creating the E2E test for this requirement. We will add it to the e2e/src/login.e2e-spec.ts file, but we are going to quickly run into an issue that needs to be solved.
Add the following test to e2e/src/login.e2e-spec.ts:
it('should take the user directly to the home page if they have logged in previously', async () => {
const input = loginPage.getKeyInput();
const loginButton = loginPage.getLoginButton();
await input.sendKeys('abcd-egfh-ijkl-mnop');
await loginButton.click();
await browser.wait(protractor.ExpectedConditions.urlContains('home'));
await browser.get('');
await browser.wait(protractor.ExpectedConditions.urlContains('home'));
expect(homePage.getModuleListItems().first().getText()).toContain(
'Module One'
);
});
In the first part of the test we do exactly what we did for the test for checking if a user can log in, but once we have successfully logged in we make a call to browser.get('');
which will reload the application. We then do nothing and wait for it to take us to the home page. This is the behaviour we want - if we have already logged in previously then the next time we load the application we should automatically be taken to the home page.
All of our tests start from a logged out state, so this is fine for now. We should be able to get this E2E test passing by implementing the functionality in the application. Once we do implement it, though, the browser is going to automatically log the user in for all of the tests which is going to break what we have currently. We will need to make a decision on how to handle this, but we will get to that later. Let's focus on getting the E2E passing for now.
If we run the E2E tests with npm run e2e
the tests will not be able to complete.
What's happening with our test is that it is logging in successfully, then it reloads the browser with browser.get('')
and then nothing happens because it is just sitting on the login screen doing nothing as the tests wait for the URL to change to /home
.
We need to implement some functionality so that on subsequent visits to the application, the user will automatically be logged in.
Extending the Auth Service
We will be extending our AuthService
to include an additional function called reauthenticate
that will handle automatically authenticating the user if there is a license key stored in local storage.
Let's create a new unit test for this.
Modify src/services/auth.service.spec.ts to reflect the following:
import { TestBed, inject, fakeAsync, tick } from '@angular/core/testing';
import {
HttpClientTestingModule,
HttpTestingController,
} from '@angular/common/http/testing';
import { IonicStorageModule } from '@ionic/storage-angular';
import { AuthService } from './auth.service';
describe('AuthService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [],
providers: [AuthService],
imports: [HttpClientTestingModule, IonicStorageModule.forRoot()],
}).compileComponents();
});
it('checkKey should make a call to the server to check the validity of a key', inject(
[AuthService, HttpTestingController],
(authService, httpMock) => {
const key = 'ewu0fef0ewuf08j3892jf98';
const mockResponse = '{"isValid": true}';
authService.checkKey(key).subscribe((result: any) => {
expect(result).toEqual(mockResponse);
});
// Expect a request to the URL
const mockReq = httpMock.expectOne('http://localhost:8080/api/check');
// Execute the request using the mockResponse data
mockReq.flush(mockResponse);
}
));
it('reauthenticate should automatically check key if in storage', inject(
[AuthService, HttpTestingController],
async (authService, httpMock) => {
const mockResponse = '{"isValid": true}';
await authService.init();
spyOn(authService._storage, 'get').and.returnValue(
Promise.resolve('abcde-fghi-jklm')
);
await authService.reauthenticate();
// Expect a request to the URL
const mockReq = httpMock.expectOne('http://localhost:8080/api/check');
// Execute the request using the mockResponse data
mockReq.flush(mockResponse);
}
));
});
We've added another unit test to the end of this file, but we have also added imports for fakeAsync
and tick
, and we have also added the IonicStorageModule
to the imports
in the TestBed configuration. Since we will be using the Storage API we need to set this up for the testing environment.
NOTE: You will also need to install the Ionic Storage API using npm install @ionic/storage-angular
Just like in the other tests, we set up a mock backend since the reauthenticate
function should be making a call to check the key against the server if one is present in storage.
We create a spy on authProvider.storage
because we want to overwrite the behaviour of get
to return our own promise with fake data (for the purpose of isolating the unit test, we don't want to retrieve the actual data from storage).
We then make a call to the reauthenticate
. We make sure that the promise has resolved and then we check that the appropriate call to the server was made.
If we run our unit tests with npm test
now, we will get this error:
AuthService reauthenticate should automatically check key if in storage FAILED
Error: <spyOn> : could not find an object to spy upon for get()
It's trying to spy on authProvider._storage
but that does not exist yet, so it's time to implement that now. First of all, we will need to set up the storage module in our app.module.ts file.
Modify src/app/app.module.ts to reflect the following:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { RouteReuseStrategy } from '@angular/router';
import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { IonicStorageModule } from '@ionic/storage-angular';
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
@NgModule({
declarations: [AppComponent],
entryComponents: [],
imports: [
BrowserModule,
IonicModule.forRoot(),
IonicStorageModule.forRoot(),
AppRoutingModule,
HttpClientModule,
],
providers: [{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }],
bootstrap: [AppComponent],
})
export class AppModule {}
Modify src/services/auth.service.ts to reflect the following:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Storage } from '@ionic/storage-angular';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class AuthService {
public _storage: Storage;
constructor(private http: HttpClient, private storage: Storage) {
this.init();
}
async init(): Promise<void> {
this._storage = await this.storage.create();
}
checkKey(key: string): Observable<any> {
const body = {
key: key,
};
return this.http.post('http://localhost:8080/api/check', body);
}
async reauthenticate(): Promise<void> {
const key = await this._storage.get('eliteLicenseKey');
if (!key) {
throw new Error('No key found');
}
this.checkKey(key).subscribe((res) => {
if (!res) {
throw new Error('Invalid key');
}
});
}
}
We've set up the Ionic Storage API now, and then we are using it in our reauthenticate
function. This function will first retrieve the license key from storage and if it exists it will make a call to the checkKey
function. If the key is valid it will resolve the promise, and if it isn't it will reject it.
If we run our unit tests with npm test
now, you will see that everything passes:
Executed 21 of 21 SUCCESS (0.371 secs / 0.357 secs)
Adding Reauthentication to the Login Page
Now that we have a function to reauthenticate a user, we need to make use of it in the login page. We want to trigger this function when the user first loads the page, and if they do have a valid license key then we want them to be taken straight to the home page.
Let's create our unit test.
Add the following unit test to src/app/login/login.page.spec.ts:
it('if the user has a valid license key in storage then they should be taken straight to the home page', fakeAsync(() => {
const authProvider = fixture.debugElement.injector.get(AuthService);
const navCtrl = fixture.debugElement.injector.get(NavController);
spyOn(navCtrl, 'navigateRoot');
spyOn(authProvider, 'reauthenticate').and.returnValue(
new Promise((resolve) => setTimeout(resolve, 0))
);
component.ngOnInit();
tick();
expect(navCtrl.navigateRoot).toHaveBeenCalledWith('/home');
}));
This is a pretty straightforward test. We are spying on navigateRoot
of the navCtrl
to see if it is called with /home
at some point during the test. We also create a spy on our reauthenticate
function so that it automatically responds with a resolved promise (again, isolating it from the actual AuthService
).
The reauthentication should occur immediately, so we will be setting it up in ngOnInit
. In the test, we make a call to ngOnInit
, we flush any microtasks with tick()
, and then we check that the root page was changed.
Thanks for checking out the preview of this lesson!
You do not have the appropriate membership to view the full lesson. If you would like full access to this module you can view membership options (or log in if you are already have an appropriate membership).