TIL: Mocking localStorage and sessionStorage in Angular Unit Tests
Today I learned how to mock localStorage
and sessionStorage
in a Service’s unit tests in Angular.
(Well, it’s not actually today that I learned this, but as a thing that I use in many projects and I still always forgot. It’s worth taking some notes 📝 so I will never forget this again … which of course, I will)
Here are the reference articles/posts I learned from
- How to mock localStorage in JavaScript unit tests? (StackOverflow)
- Angular Unit Testing part 1 — Local Storage by Bradley Gore
- localStorageMock.ts by wzr1337
Big thanks to them!
I created a companion repo for this post on GitHub. You can check the code and try it out at armno/angular-mock-localstorage.
It is based on @angular/cli
version 1.0.0-rc.4
and @angular/*
version 2.4.10
. The code is also working with Angular 4.0.0-rc.1
.
The TokenService
Let’s say we have a service in an Angular app: TokenService
which reads/writes a token into localStorage
or sessionStorage
on the browser. (From now on, I will refer to only localStorage
as they both have similar APIs)
// token.service.tsimport { Injectable } from ‘@angular/core’;@Injectable()
export class TokenService { private TOKEN_KEY = ‘id_token’; constructor() { } setAccessToken(token: string) {
localStorage.setItem(this.TOKEN_KEY, token);
} getAccessToken(): string {
return localStorage.getItem(this.TOKEN_KEY);
}
}
In this Service’s unit tests, we don’t want it to use the real localStorage
because our tests should be able to run independently from localStorage.
We are only focusing on the Service itself, and not anything else.
This is a generated spec file of TokenService
(via angular-cli)
// token.service.spec.tsimport { TestBed, inject } from '@angular/core/testing';import { TokenService } from './token.service';describe('TokenService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [TokenService]
});
}); it('should ...',
inject([TokenService], (service: TokenService) => {
expect(service).toBeTruthy();
}));
});
which I normally adapt a bit (and fix that should ...
) to
import { TestBed, inject } from '@angular/core/testing';import { TokenService } from './token.service';describe('TokenService', () => {
let service: TokenService; beforeEach(() => {
TestBed.configureTestingModule({
providers: [TokenService]
}); service = TestBed.get(TokenService);
}); it('should create the service',
() => {
expect(service).toBeTruthy();
});});
Creating mock localStorage
We can create a mock version of localStorage
inside beforeEach()
with an object with similar APIs to localStorage
itself.
beforeEach(() => {
... let store = {};
const mockLocalStorage = {
getItem: (key: string): string => {
return key in store ? store[key] : null;
},
setItem: (key: string, value: string) => {
store[key] = `${value}`;
},
removeItem: (key: string) => {
delete store[key];
},
clear: () => {
store = {};
}
};
});
Then we can use spyOn
method with and.callFake
for each mockLocalStorage
object’s methods.
beforeEach(() => {
... let store = {};
const mockLocalStorage = {
getItem: (key: string): string => {
return key in store ? store[key] : null;
},
setItem: (key: string, value: string) => {
store[key] = `${value}`;
},
removeItem: (key: string) => {
delete store[key];
},
clear: () => {
store = {};
}
}; spyOn(localStorage, 'getItem')
.and.callFake(mockLocalStorage.getItem);
spyOn(localStorage, 'setItem')
.and.callFake(mockLocalStorage.setItem);
spyOn(localStorage, 'removeItem')
.and.callFake(mockLocalStorage.removeItem);
spyOn(localStorage, 'clear')
.and.callFake(mockLocalStorage.clear);
});
This basically means: whenever localStorage.getItem
is called, instead, call mockLocalStorage.getItem
with the same arguments, and so on.
Note that .length
property and key()
method are not implemented. I personally never have to use them but there should be some examples online on how to also mock them. 😅
And finally, the tests:
describe('setAccessToken', () => {
it('should store the token in localStorage',
() => {
service.setAccessToken('sometoken');
expect(localStorage.getItem('id_token')).toEqual('sometoken');
});
});describe('getAccessToken', () => {
it('should return stored token from localStorage',
() => {
localStorage.setItem('id_token', 'anothertoken');
expect(service.getAccessToken()).toEqual('anothertoken');
});
});
This is the pattern I’ve been using for all of my Angular projects so far. Of course, feedback is welcome. 🙏