141

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.

Andreas Köberle
  • 106,652
  • 57
  • 273
  • 297
anthony sottile
  • 61,815
  • 15
  • 148
  • 207

18 Answers18

165

Here is a simple way to mock it with Jasmine:

let localStore;

beforeEach(() => {
  localStore = {};

  spyOn(window.localStorage, 'getItem').and.callFake((key) =>
    key in localStore ? localStore[key] : null
  );
  spyOn(window.localStorage, 'setItem').and.callFake(
    (key, value) => (localStore[key] = value + '')
  );
  spyOn(window.localStorage, 'clear').and.callFake(() => (localStore = {}));
});

If you want to mock the local storage in all your tests, declare the beforeEach() function shown above in the global scope of your tests (the usual place is a specHelper.js script).

Jonathan
  • 8,771
  • 4
  • 41
  • 78
Andreas Köberle
  • 106,652
  • 57
  • 273
  • 297
60

just mock the global localStorage / sessionStorage (they have the same API) for your needs.
For example:

 // Storage Mock
  function storageMock() {
    let storage = {};

    return {
      setItem: function(key, value) {
        storage[key] = value || '';
      },
      getItem: function(key) {
        return key in storage ? storage[key] : null;
      },
      removeItem: function(key) {
        delete storage[key];
      },
      get length() {
        return Object.keys(storage).length;
      },
      key: function(i) {
        const keys = Object.keys(storage);
        return keys[i] || null;
      }
    };
  }

And then what you actually do, is something like that:

// mock the localStorage
window.localStorage = storageMock();
// mock the sessionStorage
window.sessionStorage = storageMock();
tanguy_k
  • 11,307
  • 6
  • 54
  • 58
a8m
  • 9,334
  • 4
  • 37
  • 40
  • 11
    As of 2016, It seems this does not work in modern browsers (checked Chrome and Firefox); overriding `localStorage` as a whole is not possible. – jakub.g Jan 13 '16 at 17:41
  • 2
    Yeah, unfortunately this doesn't work anymore, but also I'd argue that `storage[key] || null` is incorrect. If `storage[key] === 0` it will return `null` instead. I think you could do `return key in storage ? storage[key] : null` though. – redbmk Dec 21 '16 at 15:10
  • Just used this on SO! Works like a charm - just have to change localStor back to localStorage when on a real server `function storageMock() { var storage = {}; return { setItem: function(key, value) { storage[key] = value || ''; }, getItem: function(key) { return key in storage ? storage[key] : null; }, removeItem: function(key) { delete storage[key]; }, get length() { return Object.keys(storage).length; }, key: function(i) { var keys = Object.keys(storage); return keys[i] || null; } }; } window.localStor = storageMock();` – mplungjan Jan 18 '19 at 10:17
  • 4
    @a8m I am getting error after update node to 10.15.1 that `TypeError: Cannot set property localStorage of # which has only a getter`, any idea how can I fix this ? – Tasawer Nawaz Apr 10 '19 at 07:15
  • 1
    About `setItem`, it should be `storage[key] = `${value}`` instead of `storage[key] = value || ''` because you can do `sessionStorage.setItem('foo', undefined)` and it will save undefined (or null) as a string. – tanguy_k Mar 04 '20 at 13:16
31

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

actual_kangaroo
  • 5,971
  • 2
  • 31
  • 45
23

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');
Claudijo
  • 1,321
  • 1
  • 10
  • 15
14

This is what I do...

var mock = (function() {
  var store = {};
  return {
    getItem: function(key) {
      return store[key];
    },
    setItem: function(key, value) {
      store[key] = value.toString();
    },
    clear: function() {
      store = {};
    }
  };
})();

Object.defineProperty(window, 'localStorage', { 
  value: mock,
});
ChuckJHardy
  • 6,956
  • 6
  • 35
  • 39
8

Are there any libraries out there to mock localStorage?

I just wrote one:

(function () {
    var localStorage = {};
    localStorage.setItem = function (key, val) {
         this[key] = val + '';
    }
    localStorage.getItem = function (key) {
        return this[key];
    }
    Object.defineProperty(localStorage, 'length', {
        get: function () { return Object.keys(this).length - 2; }
    });

    // Your tests here

})();

My initial testing shows that localStorage refuses to be assignable in firefox

Only in global context. With a wrapper function as above, it works just fine.

user123444555621
  • 148,182
  • 27
  • 114
  • 126
  • 1
    you can also use `var window = { localStorage: ... }` – user123444555621 Jul 15 '12 at 05:45
  • 1
    Unfortunately that means I would need to know every property I'll need and have added to the window object (and I miss out on its prototype, etc.). Including whatever jQuery may need. Unfortunately this seems like a non-solution. Oh also, the tests are testing code that uses `localStorage`, the tests don't necessarily have `localStorage` directly in them. This solution does not change the `localStorage` for other scripts so it is a non-solution. +1 for the scoping trick though – anthony sottile Jul 15 '12 at 14:14
  • 1
    You may need to adapt your code in order to make it testable. I know this is very annoying, and that's why I prefer heavy selenium testing over unit tests. – user123444555621 Jul 15 '12 at 17:19
  • This is not a valid solution. If you call any function from within that anonymous function, you will lose the reference to the mock window or mock localStorage object. The purpose of a unit test is that you DO call an outside function. So when you call your function that works with localStorage, it won't use the mock. Instead, you have to wrap the code you are testing in an anonymous function. To make it testable, have it accept the window object as a parameter. – John Kurlak Mar 06 '13 at 21:26
  • That mock has a bug: When retrieving an item that doesn't exist, getItem should return null. In the mock, it returns undefined. The correct code should be `if this.hasOwnProperty(key) return this[key] else return null` – Evan Feb 06 '15 at 22:23
  • I refined this code a bit, added tests and published as npm here: https://www.npmjs.com/package/mock-local-storage. And here is source code: https://github.com/letsrock-today/mock-local-storage/blob/master/src/mock-localstorage.js Some tests: https://github.com/letsrock-today/mock-local-storage/blob/master/test/mock-localstorage.js Module creates mock localStorage and sessionStorage on global object (window or global, which is defined). In my project I required it with mocha as this: ```mocha -r mock-localstorage``` to make global declaration available for all other code. – nikolay_turpitko Apr 18 '15 at 08:19
  • return (key in this) ? this[key] : null; – Michael Dausmann Apr 20 '18 at 09:19
6

Overwriting the localStorage property of the global window object as suggested in some of the answers won't work in most JS engines, because they declare the localStorage data property as not writable and not configurable.

However I found out that at least with PhantomJS's (version 1.9.8) WebKit version you could use the legacy API __defineGetter__ to control what happens if localStorage is accessed. Still it would be interesting if this works in other browsers as well.

var tmpStorage = window.localStorage;

// replace local storage
window.__defineGetter__('localStorage', function () {
    throw new Error("localStorage not available");
    // you could also return some other object here as a mock
});

// do your tests here    

// restore old getter to actual local storage
window.__defineGetter__('localStorage',
                        function () { return tmpStorage });

The benefit of this approach is that you would not have to modify the code you're about to test.

Conrad Calmez
  • 96
  • 1
  • 5
5

You don't have to pass the storage object to each method that uses it. Instead, you can use a configuration parameter for any module that touches the storage adapter.

Your old module

// hard to test !
export const someFunction (x) {
  window.localStorage.setItem('foo', x)
}

// hard to test !
export const anotherFunction () {
  return window.localStorage.getItem('foo')
}

Your new module with config "wrapper" function

export default function (storage) {
  return {
    someFunction (x) {
      storage.setItem('foo', x)
    }
    anotherFunction () {
      storage.getItem('foo')
    }
  }
}

When you use the module in testing code

// import mock storage adapater
const MockStorage = require('./mock-storage')

// create a new mock storage instance
const mock = new MockStorage()

// pass mock storage instance as configuration argument to your module
const myModule = require('./my-module')(mock)

// reset before each test
beforeEach(function() {
  mock.clear()
})

// your tests
it('should set foo', function() {
  myModule.someFunction('bar')
  assert.equal(mock.getItem('foo'), 'bar')
})

it('should get foo', function() {
  mock.setItem('foo', 'bar')
  assert.equal(myModule.anotherFunction(), 'bar')
})

The MockStorage class might look like this

export default class MockStorage {
  constructor () {
    this.storage = new Map()
  }
  setItem (key, value) {
    this.storage.set(key, value)
  }
  getItem (key) {
    return this.storage.get(key)
  }
  removeItem (key) {
    this.storage.delete(key)
  }
  clear () {
    this.constructor()
  }
}

When using your module in production code, instead pass the real localStorage adapter

const myModule = require('./my-module')(window.localStorage)
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • fyi to folks, this is only valid in es6: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export (but is a great solution and i cant wait until its available everywhere!) – Alex Moore-Niemi Dec 16 '16 at 02:35
  • @AlexMoore-Niemi there's very little use of ES6 here. All of it could be done using ES5 or lower with very few changes. – Mulan Dec 16 '16 at 17:12
  • yep, just pointing out `export default function` and initializing a module with an arg like that is es6 only. the pattern stands regardless. – Alex Moore-Niemi Dec 17 '16 at 02:26
  • Huh? I had to use the older style `require` to import a module and apply it to an argument in the same expression. There's no way to do that in ES6 that I know of. Otherwise I would've used ES6 `import` – Mulan Dec 17 '16 at 17:36
4

Here is an exemple using sinon spy and mock:

// window.localStorage.setItem
var spy = sinon.spy(window.localStorage, "setItem");

// You can use this in your assertions
spy.calledWith(aKey, aValue)

// Reset localStorage.setItem method    
spy.reset();



// window.localStorage.getItem
var stub = sinon.stub(window.localStorage, "getItem");
stub.returns(aValue);

// You can use this in your assertions
stub.calledWith(aKey)

// Reset localStorage.getItem method
stub.reset();
Manuel Bitto
  • 5,073
  • 6
  • 39
  • 47
4

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(...
  });
Johansrk
  • 5,160
  • 3
  • 38
  • 37
3

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.

RandomEngy
  • 14,931
  • 5
  • 70
  • 113
2

I decided to reiterate my comment to Pumbaa80's answer as separate answer so that it'll be easier to reuse it as a library.

I took Pumbaa80's code, refined it a bit, added tests and published it as an npm module here: https://www.npmjs.com/package/mock-local-storage.

Here is a source code: https://github.com/letsrock-today/mock-local-storage/blob/master/src/mock-localstorage.js

Some tests: https://github.com/letsrock-today/mock-local-storage/blob/master/test/mock-localstorage.js

Module creates mock localStorage and sessionStorage on the global object (window or global, which of them is defined).

In my other project's tests I required it with mocha as this: mocha -r mock-local-storage to make global definitions available for all code under test.

Basically, code looks like follows:

(function (glob) {

    function createStorage() {
        let s = {},
            noopCallback = () => {},
            _itemInsertionCallback = noopCallback;

        Object.defineProperty(s, 'setItem', {
            get: () => {
                return (k, v) => {
                    k = k + '';
                    _itemInsertionCallback(s.length);
                    s[k] = v + '';
                };
            }
        });
        Object.defineProperty(s, 'getItem', {
            // ...
        });
        Object.defineProperty(s, 'removeItem', {
            // ...
        });
        Object.defineProperty(s, 'clear', {
            // ...
        });
        Object.defineProperty(s, 'length', {
            get: () => {
                return Object.keys(s).length;
            }
        });
        Object.defineProperty(s, "key", {
            // ...
        });
        Object.defineProperty(s, 'itemInsertionCallback', {
            get: () => {
                return _itemInsertionCallback;
            },
            set: v => {
                if (!v || typeof v != 'function') {
                    v = noopCallback;
                }
                _itemInsertionCallback = v;
            }
        });
        return s;
    }

    glob.localStorage = createStorage();
    glob.sessionStorage = createStorage();
}(typeof window !== 'undefined' ? window : global));

Note that all methods added via Object.defineProperty so that them won't be iterated, accessed or removed as regular items and won't count in length. Also I added a way to register callback which is called when an item is about to be put into object. This callback may be used to emulate quota exceeded error in tests.

nikolay_turpitko
  • 816
  • 10
  • 12
0

Unfortunately, the only way we can mock the localStorage object in a test scenario is to change the code we're testing. You have to wrap your code in an anonymous function (which you should be doing anyway) and use "dependency injection" to pass in a reference to the window object. Something like:

(function (window) {
   // Your code
}(window.mockWindow || window));

Then, inside of your test, you can specify:

window.mockWindow = { localStorage: { ... } };
John Kurlak
  • 6,594
  • 7
  • 43
  • 59
0

This is how I like to do it. Keeps it simple.

  let localStoreMock: any = {};

  beforeEach(() => {

    angular.mock.module('yourApp');

    angular.mock.module(function ($provide: any) {

      $provide.service('localStorageService', function () {
        this.get = (key: any) => localStoreMock[key];
        this.set = (key: any, value: any) => localStoreMock[key] = value;
      });

    });
  });
0

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.

Ilker Cat
  • 1,862
  • 23
  • 17
0

For those wanting to mock localstorage and not simply spy on it, this worked for me:

Storage.prototype.getItem = jest.fn(() => 'bla');

Source: https://github.com/facebook/jest/issues/6858

Vaidas D.
  • 193
  • 3
  • 8
-1

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.

Neurotransmitter
  • 6,289
  • 2
  • 51
  • 38
-1

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

Jonathan
  • 8,771
  • 4
  • 41
  • 78