How to mock localStorage in JavaScript unit tests?

Viewed 117605

Are there any libraries out there to mock localStorage?

I've been using Sinon.JS for most of my other javascript mocking and have found it is really great.

My initial testing shows that localStorage refuses to be assignable in firefox (sadface) so I'll probably need some sort of hack around this :/

My options as of now (as I see) are as follows:

  1. Create wrapping functions that all my code uses and mock those
  2. Create some sort of (might be complicated) state management (snapshot localStorage before test, in cleanup restore snapshot) for localStorage.
  3. ??????

What do you think of these approaches and do you think there are any other better ways to go about this? Either way I'll put the resulting "library" that I end up making on github for open source goodness.

17 Answers

The current solutions will not work in Firefox. This is because localStorage is defined by the html spec as being not modifiable. You can however get around this by accessing localStorage's prototype directly.

The cross browser solution is to mock the objects on Storage.prototype e.g.

instead of spyOn(localStorage, 'setItem') use

spyOn(Storage.prototype, 'setItem')
spyOn(Storage.prototype, 'getItem')

taken from bzbarsky and teogeos's replies here https://github.com/jasmine/jasmine/issues/299

Also consider the option to inject dependencies in an object's constructor function.

var SomeObject(storage) {
  this.storge = storage || window.localStorage;
  // ...
}

SomeObject.prototype.doSomeStorageRelatedStuff = function() {
  var myValue = this.storage.getItem('myKey');
  // ...
}

// In src
var myObj = new SomeObject();

// In test
var myObj = new SomeObject(mockStorage)

In line with mocking and unit testing, I like to avoid testing the storage implementation. For instance no point in checking if length of storage increased after you set an item, etc.

Since it is obviously unreliable to replace methods on the real localStorage object, use a "dumb" mockStorage and stub the individual methods as desired, such as:

var mockStorage = {
  setItem: function() {},
  removeItem: function() {},
  key: function() {},
  getItem: function() {},
  removeItem: function() {},
  length: 0
};

// Then in test that needs to know if and how setItem was called
sinon.stub(mockStorage, 'setItem');
var myObj = new SomeObject(mockStorage);

myObj.doSomeStorageRelatedStuff();
expect(mockStorage.setItem).toHaveBeenCalledWith('myKey');

credits to https://medium.com/@armno/til-mocking-localstorage-and-sessionstorage-in-angular-unit-tests-a765abdc9d87 Make a fake localstorage, and spy on localstorage, when it is caleld

 beforeAll( () => {
    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);
  })

And here we use it

it('providing search value should return matched item', () => {
    localStorage.setItem('defaultLanguage', 'en-US');

    expect(...
  });

I found that I did not need to mock it. I could change the actual local storage to the state I wanted it via setItem, then just query the values to see if it changed via getItem. It's not quite as powerful as mocking as you can't see how many times something was changed, but it worked for my purposes.

Need to interact with stored data
A quite short approach

const store = {};
Object.defineProperty(window, 'localStorage', { 
  value: {
    getItem:(key) => store[key]},
    setItem:(key, value) => {
      store[key] = value.toString();
    },
    clear: () => {
      store = {};
    }
  },
});

Spy with Jasmine
If you just need these functions to spy on them using jasmine it will be even shorter and easier to read.

Object.defineProperty(window, 'localStorage', { 
  value: {
    getItem:(key) => {},
    setItem:(key, value) => {},
    clear: () => {},
    ...
  },
});

const spy = spyOn(localStorage, 'getItem')

Now you don't need a store at all.

I know OP specifically asked about mocking, but arguably it's better to spy rather than mock. And what if you use Object.keys(localStorage) to iterate over all available keys? You can test it like this:

const someFunction = () => {
  const localStorageKeys = Object.keys(localStorage)
  console.log('localStorageKeys', localStorageKeys)
  localStorage.removeItem('whatever')
}

and the test code will be like follows:

describe('someFunction', () => {
  it('should remove some item from the local storage', () => {
    const _localStorage = {
      foo: 'bar', fizz: 'buzz'
    }

    Object.setPrototypeOf(_localStorage, {
      removeItem: jest.fn()
    })

    jest.spyOn(global, 'localStorage', 'get').mockReturnValue(_localStorage)

    someFunction()

    expect(global.localStorage.removeItem).toHaveBeenCalledTimes(1)
    expect(global.localStorage.removeItem).toHaveBeenCalledWith('whatever')
  })
})

No need for mocks or constructors. Relatively few lines, too.

None of these answers are completely accurate or safe to use. Neither is this one but it is as accurate as I wanted without figuring out how to manipulate getters and setters.

TypeScript

const mockStorage = () => {
  for (const storage of [window.localStorage, window.sessionStorage]) {
    let store = {};

    spyOn(storage, 'getItem').and.callFake((key) =>
      key in store ? store[key] : null
    );
    spyOn(storage, 'setItem').and.callFake(
      (key, value) => (store[key] = value + '')
    );
    spyOn(storage, 'removeItem').and.callFake((key: string) => {
      delete store[key];
    });
    spyOn(storage, 'clear').and.callFake(() => (store = {}));
    spyOn(storage, 'key').and.callFake((i: number) => {
      throw new Error(`Method 'key' not implemented`);
    });
    // Storage.length is not supported
    // Property accessors are not supported
  }
};

Usage

describe('Local storage', () => {
  beforeEach(() => {
    mockStorage();
  });

  it('should cache a unit in session', () => {
    LocalStorageService.cacheUnit(testUnit);
    expect(window.sessionStorage.setItem).toHaveBeenCalledTimes(1);
    expect(window.sessionStorage.getItem(StorageKeys.units)).toContain(
      testUnit.id
    );
  });
});

Caveats

  • With localStorage you can do window.localStorage['color'] = 'red'; this will bypass the mock.
  • window.localStorage.length will bypass this mock.
  • window.localStorage.key throws in this mock as code relying on this can not be tested by this mock.
  • Mock correctly separates local and session storage.

Please also see: MDN: Web Storage API

Related