Testing Storage and Reauthentication
Dealing with complicated scenarios in E2E tests and more unit tests
PROModule Outline
- Source Code & Resources PRO
- Lesson 1: Introduction PUBLIC
- Lesson 2: Introduction to Test Driven Development PUBLIC
- Lesson 3: Testing Concepts PUBLIC
- Lesson 4: Jest and Cypress 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 with Jest and Cypress PUBLIC
- Lesson 9: Test Development Cycle PRO
- Lesson 10: Setting up Cypress & Jest in an Ionic/Angular Project 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 local storage, 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 cypress/e2e/login.cy.ts file, but we are going to quickly run into an issue that needs to be solved.
Add the following test to
cypress/e2e/login.cy.ts
:
it('should take the user directly to the home page if they have logged in previously', () => {
getKeyInput().type('abcd-egfh-ijkl-mnop');
getLoginButton().click();
cy.visit('/');
getModuleListItems().first().should('contain.text', '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 cy.visit('/')
which will reload the application to the default route. We then do nothing and wait for it to take us to the home page (where we should be able to check one of the listed module items). 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 new test will not be able to complete (remember that you can just run the logic.spec.ts
tests for now to speed things up a bit).
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 waiting for the module items to appear:
Expected to find element: [data-test="module-list-item"], but never found it.
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 { AuthService } from './auth.service';
describe('AuthService', () => {
let service: AuthService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
});
service = TestBed.inject(AuthService);
httpMock = TestBed.inject(HttpTestingController);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('checkKey should make a call to the server to check the validity of a key', () => {
const key = 'ewu0fef0ewuf08j3892jf98';
const mockResponse = '{"isValid": true}';
service.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', fakeAsync(() => {
const mockResponse = '{"isValid": true}';
jest.spyOn(service.storage, 'getItem').mockReturnValue('abcde-fghi-jklm');
service.reauthenticate();
tick();
// 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
.
Before we discuss the test itself, I want to throw in a little extra theory here in regard to fakeAsync
and async/await
. We could have also written this test using an async
callback for the it
statement, and then we could do things like await service.init()
and await authService.reauthenticate()
and it would work just fine. Instead, we are using a fakeAsync
zone and using Angular's tick()
method to advance time manually. This:
await service.reauthenticate();
is effectively the same as this:
service.reauthenticate();
tick();
The only reason I am mentioning this is to clear up any confusion as to why we are not using await
here. The main reason is that I am trying to keep things simple and consistent among tests, and using fakeAsync/tick
generally (along with the flush
and flushMicrotasks
) does give you more precise control over how your asynchronous code executes (e.g. the ability to call tick(1000)
to advanced time 1000ms
). Again, don't let this trip you up too much if you are newer to testing - just go with whatever makes sense and works for you (until it doesn't). If you need to use fakeAsync
it will become apparent when you can't achieve what you need with just async/await
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 service.storage
because we want to overwrite the behaviour of getItem
to return our 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 by calling tick
and then we check that the appropriate call to the server was made.
Let's first define dummy methods/members in our service:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class AuthService {
public storage = null;
constructor(private http: HttpClient) {}
reauthenticate() {}
public checkKey(key: string): Observable<any> {
const body = { key };
return this.http.post('http://localhost:8080/api/check', body);
}
}
and then we can watch our test fail:
FAIL src/app/services/auth.service.spec.ts
● AuthService - reauthenticate should automatically check key if in storage
TypeError: Cannot read properties of null (reading 'getItem')
41 | const mockResponse = '{"isValid": true}';
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).