Come simulare localStorage negli unit test JavaScript?


103

Ci sono biblioteche là fuori da prendere in giro localStorage?

Ho usato Sinon.JS per la maggior parte dei miei altri javascript mocking e ho trovato che è davvero fantastico.

I miei test iniziali mostrano che localStorage rifiuta di essere assegnabile in firefox (sadface) quindi probabilmente avrò bisogno di una sorta di hack attorno a questo: /

Le mie opzioni al momento (come vedo) sono le seguenti:

  1. Crea funzioni di wrapping utilizzate da tutto il mio codice e deride quelle
  2. Creare una sorta di (potrebbe essere complicata) gestione dello stato (snapshot localStorage prima del test, in cleanup restore snapshot) per localStorage.
  3. ??????

Cosa ne pensi di questi approcci e pensi che ci siano altri modi migliori per farlo? Ad ogni modo metterò la "libreria" risultante che finisco per creare su GitHub per la bontà open source.


34
Ti sei perso # 4:Profit!
Chris Laplante

Risposte:


128

Ecco un modo semplice per deriderlo con Jasmine:

beforeEach(function () {
  var store = {};

  spyOn(localStorage, 'getItem').andCallFake(function (key) {
    return store[key];
  });
  spyOn(localStorage, 'setItem').andCallFake(function (key, value) {
    return store[key] = value + '';
  });
  spyOn(localStorage, 'clear').andCallFake(function () {
      store = {};
  });
});

Se vuoi deridere l'archiviazione locale in tutti i tuoi test, dichiara la beforeEach()funzione mostrata sopra nell'ambito globale dei tuoi test (il solito posto è uno script specHelper.js ).


1
+1 - potresti farlo anche con sinon. La chiave è perché preoccuparsi di prendere in giro per deridere l'intero oggetto localStorage, semplicemente deridere i metodi (getItem e / o setItem) che ti interessano.
s1mm0t

6
Attenzione: sembra esserci un problema con questa soluzione in Firefox: github.com/pivotal/jasmine/issues/299
cthulhu

4
Ottengo un ReferenceError: localStorage is not defined(test in esecuzione utilizzando FB Jest e npm) ... qualche idea su come lavorare?
FeifanZ

1
Prova a spiarewindow.localStorage
Benj

22
andCallFakecambiato in and.callFakein gelsomino 2. +
Venugopal

51

prendi in giro localStorage / sessionStorage globali (hanno la stessa API) per le tue esigenze.
Per esempio:

 // 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;
      }
    };
  }

E poi quello che fai effettivamente è qualcosa del genere:

// mock the localStorage
window.localStorage = storageMock();
// mock the sessionStorage
window.sessionStorage = storageMock();

1
Suggerimento di modifica: getItemdeve essere restituito nullquando il valore non esiste return storage[key] || null;:;
cyberwombat

8
A partire dal 2016, sembra che questo non funzioni nei browser moderni (controllato Chrome e Firefox); l'override localStoragenel suo insieme non è possibile.
jakub.g

2
Sì, sfortunatamente questo non funziona più, ma direi anche che storage[key] || nullnon è corretto. Se invece storage[key] === 0tornerà null. Penso che potresti fare return key in storage ? storage[key] : nullcomunque.
redbmk

L'ho appena usato su SO! Funziona a meraviglia: devi solo cambiare localStor in localStorage quando sei su un server realefunction 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

2
@ a8m Ricevo un errore dopo l'aggiornamento del nodo a 10.15.1 TypeError: Cannot set property localStorage of #<Window> which has only a getter, qualche idea su come posso risolverlo?
Tasawer Nawaz

19

Considera anche l'opzione per inserire dipendenze nella funzione di costruzione di un oggetto.

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 linea con il mocking e il test unitario, mi piace evitare di testare l'implementazione dello storage. Ad esempio, non ha senso controllare se la durata dell'archiviazione è aumentata dopo aver impostato un elemento, ecc.

Poiché è ovviamente inaffidabile sostituire i metodi sull'oggetto localStorage reale, utilizzare un mockStorage "stupido" e stub i singoli metodi come desiderato, ad esempio:

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');

1
Mi rendo conto che è passato un po 'di tempo dall'ultima volta che ho esaminato questa domanda, ma in realtà è quello che ho finito per fare.
Anthony Sottile

1
Questa è l'unica soluzione utile, poiché non ha un rischio così elevato di interruzione del tempo.
oligofren

14

Questo è ciò che faccio...

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,
});

12

Le soluzioni attuali non funzioneranno in Firefox. Questo perché localStorage è definito dalle specifiche html come non modificabile. Puoi comunque aggirare il problema accedendo direttamente al prototipo di localStorage.

La soluzione cross browser consiste nel deridere gli oggetti ad Storage.prototypees

invece di spyOn (localStorage, 'setItem') usa

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

tratto dalle risposte di bzbarsky e teogeos qui https://github.com/jasmine/jasmine/issues/299


1
Il tuo commento dovrebbe ricevere più Mi piace. Grazie!
LorisBachert

6

Ci sono biblioteche là fuori da prendere in giro localStorage?

Ne ho appena scritto uno:

(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

})();

I miei test iniziali mostrano che localStorage rifiuta di essere assegnabile in Firefox

Solo in un contesto globale. Con una funzione wrapper come sopra, funziona perfettamente.


1
puoi anche usarevar window = { localStorage: ... }
user123444555621

1
Sfortunatamente questo significa che avrei bisogno di conoscere ogni proprietà di cui avrò bisogno e che avrei aggiunto all'oggetto finestra (e mi perdo il suo prototipo, ecc.). Incluso tutto ciò che potrebbe essere necessario jQuery. Purtroppo questa sembra una non soluzione. Oh, inoltre, i test stanno testando il codice che utilizza localStorage, i test non hanno necessariamente localStoragedirettamente al loro interno. Questa soluzione non modifica la localStorageper altri script, quindi non è una soluzione. +1 per lo scoping trick però
Anthony Sottile

1
Potrebbe essere necessario adattare il codice per renderlo testabile. So che questo è molto fastidioso, ed è per questo che preferisco test pesanti sul selenio rispetto ai test unitari.
user123444555621

Questa non è una soluzione valida. Se chiami una funzione dall'interno di quella funzione anonima, perderai il riferimento alla finta finestra o finto oggetto localStorage. Lo scopo di uno unit test è che tu chiami una funzione esterna. Quindi, quando chiami la tua funzione che funziona con localStorage, non userà il mock. Invece, devi racchiudere il codice che stai testando in una funzione anonima. Per renderlo testabile, fai in modo che accetti l'oggetto finestra come parametro.
John Kurlak

Quel mock ha un bug: quando si recupera un elemento che non esiste, getItem dovrebbe restituire null. Nella simulazione, restituisce indefinito. Il codice corretto dovrebbe essereif this.hasOwnProperty(key) return this[key] else return null
Evan

4

Ecco un esempio che utilizza sinon spy e 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();

4

La sovrascrittura della localStorageproprietà windowdell'oggetto globale come suggerito in alcune delle risposte non funzionerà nella maggior parte dei motori JS, perché dichiarano la localStorageproprietà dei dati come non scrivibile e non configurabile.

Tuttavia ho scoperto che almeno con la versione WebKit di PhantomJS (versione 1.9.8) è possibile utilizzare l'API legacy __defineGetter__per controllare cosa succede se localStoragesi accede. Tuttavia sarebbe interessante se questo funzionasse anche su altri browser.

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 });

Il vantaggio di questo approccio è che non dovresti modificare il codice che stai per testare.


Ho appena notato che questo non funzionerà in PhantomJS 2.1.1. ;)
Conrad Calmez

4

Non è necessario passare l'oggetto di archiviazione a ogni metodo che lo utilizza. È invece possibile utilizzare un parametro di configurazione per qualsiasi modulo che tocchi l'adattatore di archiviazione.

Il tuo vecchio modulo

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

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

Il tuo nuovo modulo con la funzione "wrapper" di configurazione

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

Quando usi il modulo per testare il codice

// 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')
})

La MockStorageclasse potrebbe assomigliare a questa

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()
  }
}

Quando si utilizza il modulo nel codice di produzione, passare invece l'adattatore localStorage reale

const myModule = require('./my-module')(window.localStorage)

Cordiali saluti, questo è valido solo in es6: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… (ma è un'ottima soluzione e non vedo l'ora che sia disponibile ovunque!)
Alex Moore- Niemi

@ AlexMoore-Niemi qui si usa pochissimo ES6. Tutto potrebbe essere fatto utilizzando ES5 o inferiore con pochissime modifiche.
Grazie

sì, solo evidenziando export default functione inizializzando un modulo con un argomento come questo è solo es6. il modello sta indipendentemente.
Alex Moore-Niemi

Eh? Ho dovuto usare il vecchio stile requireper importare un modulo e applicarlo a un argomento nella stessa espressione. Non c'è modo di farlo in ES6 che io sappia. Altrimenti avrei usato ES6import
Grazie

2

Ho deciso di ribadire il mio commento alla risposta di Pumbaa80 come risposta separata in modo che sia più facile riutilizzarla come libreria.

Ho preso il codice di Pumbaa80, l'ho perfezionato un po ', ho aggiunto dei test e l'ho pubblicato come modulo npm qui: https://www.npmjs.com/package/mock-local-storage .

Ecco un codice sorgente: https://github.com/letsrock-today/mock-local-storage/blob/master/src/mock-localstorage.js

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

Il modulo crea finto localStorage e sessionStorage sull'oggetto globale (finestra o globale, quale di essi è definito).

Negli altri test del mio progetto l'ho richiesto con mocha come questo: mocha -r mock-local-storageper rendere disponibili definizioni globali per tutto il codice sotto test.

Fondamentalmente, il codice ha il seguente aspetto:

(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));

Nota che tutti i metodi aggiunti tramite in Object.definePropertymodo che non vengano iterati, acceduti o rimossi come elementi normali e non conteranno in lunghezza. Inoltre ho aggiunto un modo per registrare il callback che viene chiamato quando un elemento sta per essere inserito nell'oggetto. Questo callback può essere utilizzato per emulare l'errore di quota superata nei test.


2

Ho scoperto che non avevo bisogno di deriderlo. Potrei cambiare l'effettiva memoria locale nello stato che volevo tramite setItem, quindi interrogare i valori per vedere se è cambiato tramite getItem. Non è così potente come beffardo come non puoi vedere quante volte qualcosa è stato cambiato, ma ha funzionato per i miei scopi.


0

Sfortunatamente, l'unico modo in cui possiamo deridere l'oggetto localStorage in uno scenario di test è cambiare il codice che stiamo testando. Devi racchiudere il tuo codice in una funzione anonima (cosa che dovresti fare comunque) e usare "iniezione di dipendenze" per passare un riferimento all'oggetto finestra. Qualcosa di simile a:

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

Quindi, all'interno del tuo test, puoi specificare:

window.mockWindow = { localStorage: { ... } };

0

È così che mi piace farlo. Mantiene le cose semplici.

  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

crediti a https://medium.com/@armno/til-mocking-localstorage-and-sessionstorage-in-angular-unit-tests-a765abdc9d87 Crea un localstorage falso e spiare localstorage, quando viene chiamato

 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);
  })

E qui lo usiamo

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

    expect(...
  });
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.