web-dev-qa-db-de.com

Wie wird localStorage in JavaScript-Komponententests simuliert?

Gibt es Bibliotheken, die localStorage nachahmen?

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

Meine ersten Tests zeigen, dass localStorage es ablehnt, in Firefox (sadface) zugeteilt zu werden. Ich werde wahrscheinlich eine Art Hack um dieses Problem benötigen: /

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

  1. Erstellen Sie Wrapping-Funktionen, die in meinem gesamten Code verwendet werden, und machen Sie diese zu verspotten
  2. Erstellen Sie eine Art (möglicherweise komplizierter) Statusverwaltung (Snapshot localStorage vor dem Test, im Bereinigungs-Restore-Snapshot) für localStorage.
  3. ??????

Was halten Sie von diesen Ansätzen und glauben Sie, dass es andere Wege gibt, um dies zu erreichen? Wie auch immer, ich werde die resultierende "Bibliothek", die ich am Ende auf Github mache, für Open Source-Güte einsetzen.

81
Anthony Sottile

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 simulieren möchten, deklarieren Sie die oben gezeigte Funktion beforeEach() im globalen Bereich Ihrer Tests (der übliche Ort ist ein specHelper.js script).

108

verspotten Sie einfach das globale localStorage/sessionStorage (sie haben dieselbe API) für Ihre Anforderungen.
Zum Beispiel:

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

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

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

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 Zusammenhang mit der Verspottung und dem Testen von Einheiten möchte ich es vermeiden, die Speicherimplementierung zu testen. Zum Beispiel macht es keinen Sinn, zu prüfen, ob die Speicherdauer nach dem Einstellen eines Elements erhöht wurde usw. 

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

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

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

Gibt es Bibliotheken, die localStorage nachahmen?

Ich habe gerade eine 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 zeigen, dass localStorage sich in Firefox nicht zuordnen lässt

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

6

Hier ist ein Beispiel mit Sinon Spion und Spott:

// 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
Manuel Bitto

Das Überschreiben der localStorage -Eigenschaft des globalen window-Objekts, wie in einigen Antworten vorgeschlagen, funktioniert in den meisten JS-Engines nicht, da sie die localStorage -Dateneigenschaft als nicht schreibbar und nicht konfigurierbar deklarieren.

Ich fand jedoch heraus, dass Sie zumindest mit der WebKit-Version von PhantomJS (Version 1.9.8) die ältere API __defineGetter__ verwenden können, um zu steuern, was passiert, wenn auf localStorage zugegriffen wird. Dennoch 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.

4
Conrad Calmez

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

Die Cross-Browser-Lösung besteht darin, die Objekte in Storage.prototype, z.

anstelle von spyOn (localStorage, 'setItem') verwenden 

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

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

3
roo2

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 der Config-Wrapper-Funktion

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

Wenn Sie das Modul im Testcode 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 MockStorage-Klasse 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)
3
user633183

Ich entschied mich dazu, meinen Kommentar zu Pumbaa80s Antwort als separate Antwort zu wiederholen, damit es einfacher ist, ihn als Bibliothek wiederzuverwenden.

Ich habe den Code von Pumbaa80 genommen, etwas verfeinert, Tests hinzugefügt und als npm-Modul hier veröffentlicht: 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 die Muster "localStorage" und "sessionStorage" für das globale Objekt (Fenster oder globales Objekt, welches von ihnen definiert ist).

In den Tests meines anderen Projekts benötigte ich dies mit mocha: mocha -r mock-local-storage, um globale Definitionen für den gesamten getesteten Code verfügbar zu machen.

Grundsätzlich sieht der 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 über Object.defineProperty hinzugefügten Methoden nicht als reguläre Elemente iteriert, darauf zugegriffen oder entfernt werden und ihre Länge nicht zählt. Ich habe auch eine Möglichkeit hinzugefügt, einen Callback zu registrieren, der aufgerufen wird, wenn ein Element in ein Objekt eingefügt wird. Dieser Rückruf kann verwendet werden, um einen Fehler bei Quotüberschreitung in Tests zu emulieren.

1

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

    });
  });

Ich stellte fest, dass ich mich nicht lustig machen musste. Ich könnte den tatsächlichen lokalen Speicher über setItem in den gewünschten Zustand versetzen und dann die Werte abfragen, um festzustellen, ob sie sich über getItem geändert haben. Es ist nicht ganz so schlagkräftig wie das Verspotten, da man nicht sehen kann, wie oft etwas geändert wurde, aber es hat für meine Zwecke funktioniert.

0
RandomEngy

Leider können Sie das localStorage-Objekt in einem Testszenario nur verspotten, indem Sie den getesteten Code ändern. Sie müssen Ihren Code in eine anonyme Funktion einschließen (was Sie sowieso tun sollten) und mit "Abhängigkeitsinjektion" eine Referenz an das Fensterobjekt übergeben. So etwas wie:

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

Im Test können Sie dann Folgendes angeben:

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