Wie verspottet man localStorage in JavaScript-Unit-Tests?

103

Gibt es da draußen Bibliotheken zum Verspotten localStorage?

Ich habe Sinon.JS für die meisten meiner anderen Javascript-Verspottungen verwendet und festgestellt, dass es wirklich großartig ist.

Meine ersten Tests haben gezeigt, dass localStorage sich weigert, in Firefox (sadface) zuweisbar zu sein, daher brauche ich wahrscheinlich eine Art Hack, um dies zu umgehen: /

Meine Optionen sind ab sofort (wie ich sehe) wie folgt:

  1. Erstellen Sie Wrapping-Funktionen, die mein gesamter Code verwendet, und verspotten Sie diese
  2. Erstellen Sie für localStorage eine Art (möglicherweise kompliziertes) Statusmanagement (Snapshot localStorage vor dem Test, im Bereinigungs-Wiederherstellungs-Snapshot).
  3. ??????

Was halten Sie von diesen Ansätzen und glauben Sie, dass es andere bessere Möglichkeiten gibt, dies zu tun? In jedem Fall werde ich die resultierende "Bibliothek", die ich am Ende mache, für Open Source-Güte auf Github stellen.

Anthony Sottile
quelle
34
Sie haben # 4 verpasst:Profit!
Chris Laplante

Antworten:

128

Hier ist eine einfache Möglichkeit, es mit Jasmine zu verspotten:

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

Wenn Sie den lokalen Speicher in all Ihren Tests verspotten möchten, deklarieren Sie die beforeEach()oben gezeigte Funktion im globalen Bereich Ihrer Tests (der übliche Ort ist ein specHelper.js- Skript).

Andreas Köberle
quelle
1
+1 - das kannst du auch mit sinon machen. Der Schlüssel ist, warum Sie sich die Mühe machen, das gesamte localStorage-Objekt zu verspotten. Verspotten Sie einfach die Methoden (getItem und / oder setItem), an denen Sie interessiert sind.
s1mm0t
6
Heads up: Es scheint ein Problem mit dieser Lösung in Firefox zu geben: github.com/pivotal/jasmine/issues/299
fwielstra
4
Ich bekomme eine ReferenceError: localStorage is not defined(laufende Tests mit FB Jest und npm)… irgendwelche Ideen, wie man das umgeht?
FeifanZ
1
Versuchen Sie es auszuspionierenwindow.localStorage
Benj
21
andCallFakegeändert zu and.callFakein Jasmin 2. +
Venugopal
51

Verspotten Sie einfach den globalen localStorage / sessionStorage (sie haben dieselbe API) für Ihre Anforderungen.
Beispielsweise:

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

Und was Sie dann tatsächlich tun, ist so etwas:

// mock the localStorage
window.localStorage = storageMock();
// mock the sessionStorage
window.sessionStorage = storageMock();
a8m
quelle
1
Vorschlag bearbeiten: getItemmuss zurückgegeben werden, nullwenn kein Wert vorhanden ist : return storage[key] || null;;
Cyberwombat
8
Ab 2016 scheint dies in modernen Browsern nicht zu funktionieren (Chrome und Firefox aktiviert). ein Überschreiben localStorageals Ganzes ist nicht möglich.
Jakub.g
2
Ja, leider funktioniert das nicht mehr, aber ich würde auch argumentieren, dass storage[key] || nulldas falsch ist. Wenn storage[key] === 0es nullstattdessen zurückkehren wird. Ich denke du könntest es aber tun return key in storage ? storage[key] : null.
Redbmk
Ich habe es gerade bei SO verwendet! Funktioniert wie ein Zauber - Sie müssen localStor nur wieder in localStorage ändern, wenn Sie sich auf einem echten Server befindenfunction 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 Ich erhalte nach dem Update des Knotens auf 10.15.1 eine Fehlermeldung. TypeError: Cannot set property localStorage of #<Window> which has only a getterWie kann ich das beheben?
Tasawer Nawaz
19

Berücksichtigen Sie auch die Option, Abhängigkeiten in die Konstruktorfunktion eines Objekts einzufügen.

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)

Im Einklang mit Verspottungen und Unit-Tests möchte ich das Testen der Speicherimplementierung vermeiden. Zum Beispiel macht es keinen Sinn zu überprüfen, ob sich die Speicherlänge nach dem Festlegen eines Elements usw. erhöht hat.

Da es offensichtlich unzuverlässig ist, Methoden für das reale localStorage-Objekt zu ersetzen, verwenden Sie einen "dummen" mockStorage und stubben Sie die einzelnen Methoden wie gewünscht, z.

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
quelle
1
Mir ist klar, dass es eine Weile her ist, seit ich mir diese Frage angesehen habe - aber genau das habe ich letztendlich getan.
Anthony Sottile
1
Dies ist die einzig lohnende Lösung, da das Risiko eines Zeitbruchs nicht so hoch ist.
Oligofren
14

Das ist was ich mache...

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
quelle
11

Die aktuellen Lösungen funktionieren in Firefox nicht. Dies liegt daran, dass localStorage in der HTML-Spezifikation als nicht änderbar definiert ist. Sie können dies jedoch umgehen, indem Sie direkt auf den Prototyp von localStorage zugreifen.

Die Cross-Browser-Lösung besteht darin, die Objekte Storage.prototypez

anstelle von spyOn (localStorage, 'setItem') verwenden

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

entnommen aus den Antworten von bzbarsky und teogeos hier https://github.com/jasmine/jasmine/issues/299

roo2
quelle
1
Dein Kommentar sollte mehr Likes bekommen. Danke dir!
LorisBachert
6

Gibt es da draußen Bibliotheken zum Verspotten localStorage?

Ich habe gerade einen geschrieben:

(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

})();

Meine ersten Tests haben gezeigt, dass localStorage sich weigert, in Firefox zuweisbar zu sein

Nur im globalen Kontext. Mit einer Wrapper-Funktion wie oben funktioniert es einwandfrei.

user123444555621
quelle
1
Sie können auch verwendenvar window = { localStorage: ... }
user123444555621
1
Leider bedeutet dies, dass ich jede Eigenschaft kennen muss, die ich benötige und dem Fensterobjekt hinzugefügt habe (und ich vermisse den Prototyp usw.). Einschließlich der von jQuery benötigten Daten. Leider scheint dies keine Lösung zu sein. Ach localStorageja , die Tests sind Testcode, der verwendet wird , die Tests müssen nicht unbedingt localStoragedirekt in ihnen enthalten sein. Diese Lösung ändert nichts localStoragefür andere Skripte, sodass es sich nicht um eine Lösung handelt. +1 für den Scoping-Trick
Anthony Sottile
1
Möglicherweise müssen Sie Ihren Code anpassen, um ihn testbar zu machen. Ich weiß, dass dies sehr ärgerlich ist, und deshalb bevorzuge ich Tests mit schwerem Selen gegenüber Unit-Tests.
user123444555621
Dies ist keine gültige Lösung. Wenn Sie eine Funktion innerhalb dieser anonymen Funktion aufrufen, verlieren Sie den Verweis auf das Scheinfenster oder das Scheinobjekt localStorage. Der Zweck eines Komponententests besteht darin, dass Sie eine externe Funktion aufrufen. Wenn Sie also Ihre Funktion aufrufen, die mit localStorage funktioniert, wird der Mock nicht verwendet. Stattdessen müssen Sie den zu testenden Code in eine anonyme Funktion einschließen. Lassen Sie das Fensterobjekt als Parameter akzeptieren, um es testbar zu machen.
John Kurlak
Dieses Modell hat einen Fehler: Wenn Sie ein nicht vorhandenes Element abrufen, sollte getItem null zurückgeben. Im Mock wird undefiniert zurückgegeben. Der richtige Code sollte seinif this.hasOwnProperty(key) return this[key] else return null
Evan
4

Hier ist ein Beispiel mit Sinon Spy und 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
quelle
4

Das Überschreiben der localStorageEigenschaft des globalen windowObjekts, wie in einigen Antworten vorgeschlagen, funktioniert in den meisten JS-Engines nicht, da die localStorageDateneigenschaft als nicht beschreibbar und nicht konfigurierbar deklariert wird .

Ich fand jedoch heraus, dass Sie zumindest mit der WebKit-Version von PhantomJS (Version 1.9.8) die Legacy-API verwenden können, um __defineGetter__zu steuern, was passiert, wenn auf sie localStoragezugegriffen wird. Trotzdem wäre es interessant, wenn dies auch in anderen Browsern funktioniert.

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

Der Vorteil dieses Ansatzes besteht darin, dass Sie den zu testenden Code nicht ändern müssen.

Conrad Calmez
quelle
Ich habe gerade bemerkt, dass dies in PhantomJS 2.1.1 nicht funktioniert. ;)
Conrad Calmez
4

Sie müssen das Speicherobjekt nicht an jede Methode übergeben, die es verwendet. Stattdessen können Sie einen Konfigurationsparameter für jedes Modul verwenden, das den Speicheradapter berührt.

Dein altes Modul

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

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

Ihr neues Modul mit Konfigurationsfunktion "Wrapper"

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

Wenn Sie das Modul zum Testen von Code verwenden

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

Die MockStorageKlasse könnte so aussehen

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

Wenn Sie Ihr Modul im Produktionscode verwenden, übergeben Sie stattdessen den echten localStorage-Adapter

const myModule = require('./my-module')(window.localStorage)
Danke dir
quelle
Zu Ihrer Information, dies ist nur in es6 gültig: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… (aber es ist eine großartige Lösung und ich kann es kaum erwarten, bis sie überall verfügbar ist!)
Alex Moore- Niemi
@ AlexMoore-Niemi ES6 wird hier nur sehr wenig verwendet. All dies kann mit ES5 oder niedriger mit sehr wenigen Änderungen durchgeführt werden.
Danke
Ja, nur darauf hinzuweisen export default functionund ein Modul mit einem solchen Argument zu initialisieren, ist nur es6. Das Muster steht trotzdem.
Alex Moore-Niemi
Huh? Ich musste den älteren Stil verwenden require, um ein Modul zu importieren und es auf ein Argument im selben Ausdruck anzuwenden. In ES6 gibt es keine Möglichkeit, das zu tun, von der ich weiß. Sonst hätte ich ES6 verwendetimport
Danke
2

Ich habe beschlossen, meinen Kommentar zur Antwort von Pumbaa80 als separate Antwort zu wiederholen, damit es einfacher ist, ihn als Bibliothek wiederzuverwenden.

Ich nahm den Code von Pumbaa80, verfeinerte ihn ein wenig, fügte Tests hinzu und veröffentlichte ihn hier als npm-Modul: https://www.npmjs.com/package/mock-local-storage .

Hier ist ein Quellcode: https://github.com/letsrock-today/mock-local-storage/blob/master/src/mock-localstorage.js

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

Das Modul erstellt Mock LocalStorage und SessionStorage für das globale Objekt (Fenster oder global, welches davon definiert ist).

In den Tests meines anderen Projekts habe ich es mit Mokka wie folgt benötigt: mocha -r mock-local-storageum globale Definitionen für den gesamten getesteten Code verfügbar zu machen.

Grundsätzlich sieht Code wie folgt aus:

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

Beachten Sie, dass alle Methoden, die über hinzugefügt wurden, Object.definePropertydamit sie nicht als reguläre Elemente iteriert, aufgerufen oder entfernt werden und nicht in der Länge zählen. Außerdem habe ich eine Möglichkeit hinzugefügt, einen Rückruf zu registrieren, der aufgerufen wird, wenn ein Element in ein Objekt eingefügt werden soll. Dieser Rückruf kann verwendet werden, um einen Fehler zu emulieren, bei dem das Kontingent überschritten wurde.

nikolay_turpitko
quelle
2

Ich stellte fest, dass ich mich nicht darüber lustig machen musste. Ich könnte den tatsächlichen lokalen Speicher in den Status ändern, über den ich ihn haben möchte setItem, und dann einfach die Werte abfragen, um festzustellen, ob er sich über geändert hat getItem. Es ist nicht ganz so mächtig wie spöttisch, da man nicht sehen kann, wie oft etwas geändert wurde, aber es hat für meine Zwecke funktioniert.

RandomEngy
quelle
0

Leider können wir das localStorage-Objekt in einem Testszenario nur verspotten, indem wir den Code ändern, den wir testen. Sie müssen Ihren Code in eine anonyme Funktion einschließen (was Sie sowieso tun sollten) und "Abhängigkeitsinjektion" verwenden, um einen Verweis auf das Fensterobjekt zu übergeben. Etwas wie:

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

Innerhalb Ihres Tests können Sie dann Folgendes angeben:

window.mockWindow = { localStorage: { ... } };
John Kurlak
quelle
0

So mache ich es gerne. Hält es einfach.

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

    });
  });
Eduardo La Hoz Miranda
quelle
0

Credits an https://medium.com/@armno/til-mocking-localstorage-and-sessionstorage-in-angular-unit-tests-a765abdc9d87 Machen Sie einen gefälschten lokalen Speicher und spionieren Sie den lokalen Speicher aus, wenn er gespeichert ist

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

Und hier benutzen wir es

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

    expect(...
  });
Johansrk
quelle