Lesson 18

Testing Storage and Reauthentication

Dealing with complicated scenarios in E2E tests and more unit tests

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}';
PRO

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).