Jak mock localStorage w testach jednostkowych JavaScript?

103

Czy są jakieś biblioteki, z których można kpić localStorage?

Używam Sinon.JS przez większość mojego innego kpin JavaScript i odkryli, że jest naprawdę świetny.

Moje wstępne testy pokazują, że localStorage odmawia przypisania w Firefoksie (sadface), więc prawdopodobnie będę potrzebował jakiegoś hacka wokół tego: /

Moje opcje na razie (jak widzę) są następujące:

  1. Twórz funkcje opakowujące, których używa cały mój kod i mockuj je
  2. Utwórz jakiś rodzaj (może być skomplikowany) zarządzania stanem (migawka localStorage przed testem, w migawce przywracania czyszczenia) dla localStorage.
  3. ??????

Co myślisz o tych podejściach i czy uważasz, że są inne lepsze sposoby, aby to osiągnąć? Tak czy inaczej, umieszczę wynikową „bibliotekę”, którą ostatecznie utworzę na githubie dla dobroci open source.

Anthony Sottile
źródło
34
Przegapiłeś # 4:Profit!
Chris Laplante

Odpowiedzi:

128

Oto prosty sposób na kpiny z 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 = {};
  });
});

Jeśli chcesz mockować pamięć lokalną we wszystkich swoich testach, zadeklaruj beforeEach()funkcję pokazaną powyżej w globalnym zakresie testów (zwykle jest to skrypt specHelper.js ).

Andreas Köberle
źródło
1
+1 - możesz to również zrobić z sinonem. Kluczem jest, dlaczego przeszkadza wiążąc drwić cały obiekt localStorage tylko drwić metod (GetItem i / lub setItem) jesteś zainteresowany.
s1mm0t
6
Uwaga
cthulhu
4
Dostaję ReferenceError: localStorage is not defined(uruchomienie testów przy użyciu FB Jest i npm)… jakieś pomysły jak obejść?
FeifanZ
1
Spróbuj szpiegowaćwindow.localStorage
Benj,
22
andCallFakezmieniono na and.callFakejaśmin 2. +
Venugopal
51

po prostu mock globalny localStorage / sessionStorage (mają to samo API) dla twoich potrzeb.
Na przykład:

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

A potem to, co faktycznie robisz, jest takie:

// mock the localStorage
window.localStorage = storageMock();
// mock the sessionStorage
window.sessionStorage = storageMock();
a8m
źródło
1
Sugestia edycji: getItemmusi zostać zwrócona, nullgdy wartość nie istnieje return storage[key] || null;:;
cyberwombat
8
Od 2016 roku wygląda na to, że nie działa to w nowoczesnych przeglądarkach (sprawdzono Chrome i Firefox); nadpisywanie localStoragejako całość nie jest możliwe.
jakub.g
2
Tak, niestety to już nie działa, ale też uważam, że storage[key] || nullto nieprawda. Jeśli zamiast tego storage[key] === 0wróci null. Myślę, że mógłbyś to zrobić return key in storage ? storage[key] : null.
redbmk
Właśnie użyłem tego na SO! Działa jak czar - wystarczy zmienić localStor plecami do localStorage gdy na prawdziwym serwerzefunction 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 Otrzymuję błąd po aktualizacji węzła do wersji 10.15.1, czy masz TypeError: Cannot set property localStorage of #<Window> which has only a getterjakiś pomysł, jak to naprawić?
Tasawer Nawaz
19

Rozważ także opcję wstrzyknięcia zależności w funkcji konstruktora obiektu.

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)

Zgodnie z mockingiem i testowaniem jednostkowym wolę unikać testowania implementacji pamięci masowej. Na przykład nie ma sensu sprawdzać, czy długość przechowywania wzrosła po ustawieniu przedmiotu itp.

Ponieważ zastąpienie metod w rzeczywistym obiekcie localStorage jest oczywiście zawodne, użyj „głupiego” mockStorage i odetnij poszczególne metody zgodnie z potrzebami, na przykład:

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
źródło
1
Zdaję sobie sprawę, że minęło trochę czasu, odkąd spojrzałem na to pytanie - ale tak naprawdę to właśnie zrobiłem.
Anthony Sottile
1
To jedyne opłacalne rozwiązanie, ponieważ nie ma tak dużego ryzyka zerwania w czasie.
oligofren
14

Tym się właśnie zajmuję...

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
źródło
12

Obecne rozwiązania nie będą działać w przeglądarce Firefox. Dzieje się tak, ponieważ localStorage jest zdefiniowane w specyfikacji HTML jako niemodyfikowalne. Możesz jednak obejść ten problem, uzyskując bezpośredni dostęp do prototypu localStorage.

Rozwiązaniem cross-browser jest mockowanie obiektów Storage.prototypenp

zamiast spyOn (localStorage, 'setItem') użyj

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

zaczerpnięte z odpowiedzi bzbarsky'ego i teogeosa tutaj https://github.com/jasmine/jasmine/issues/299

roo2
źródło
1
Twój komentarz powinien dostać więcej polubień. Dziękuję Ci!
LorisBachert
6

Czy są jakieś biblioteki, z których można kpić localStorage?

Właśnie napisałem jeden:

(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

})();

Moje wstępne testy pokazują, że localStorage odmawia przypisania w przeglądarce Firefox

Tylko w kontekście globalnym. Z funkcją owijania, jak powyżej, działa dobrze.

user123444555621
źródło
1
możesz również użyćvar window = { localStorage: ... }
user123444555621
1
Niestety oznacza to, że musiałbym znać każdą właściwość, której potrzebuję i którą dodałem do obiektu okna (i brakuje mi jej prototypu itp.). Zawiera wszystko, czego może potrzebować jQuery. Niestety wydaje się, że nie jest to rozwiązanie. Aha, testy testują kod, który używa localStorage, a testy niekoniecznie mają localStoragew sobie bezpośrednio. To rozwiązanie nie zmienia localStoragedla innych skryptów, więc nie jest rozwiązaniem. Jednak +1 dla sztuczki z celownikiem
Anthony Sottile
1
Może być konieczne dostosowanie kodu, aby był testowalny. Wiem, że jest to bardzo denerwujące i dlatego wolę ciężkie testy selenu od testów jednostkowych.
user123444555621
To nie jest prawidłowe rozwiązanie. Jeśli wywołasz jakąkolwiek funkcję z tej anonimowej funkcji, utracisz odwołanie do makiety okna lub udanego obiektu localStorage. Celem testu jednostkowego jest wywołanie funkcji zewnętrznej. Więc kiedy wywołujesz swoją funkcję, która działa z localStorage, nie użyje ona makiety. Zamiast tego musisz opakować testowany kod w anonimową funkcję. Aby było to testowalne, poproś o akceptację obiektu okna jako parametru.
John Kurlak,
Ta makieta zawiera błąd: podczas pobierania elementu, który nie istnieje, funkcja getItem powinna zwrócić wartość null. W makiecie zwraca undefined. Prawidłowy kod powinien wyglądać następującoif this.hasOwnProperty(key) return this[key] else return null
Evan,
4

Oto przykład użycia 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
źródło
4

Nadpisywanie localStoragewłaściwości windowobiektu globalnego, jak sugerowano w niektórych odpowiedziach, nie zadziała w większości silników JS, ponieważ deklarują one localStoragewłaściwość data jako niepisywalną i konfigurowalną.

Jednak dowiedziałem się, że przynajmniej z wersją WebKit PhantomJS (wersja 1.9.8) można użyć starszego interfejsu API, __defineGetter__aby kontrolować, co się stanie, jeśli localStoragezostanie uzyskany dostęp. Mimo to byłoby interesujące, gdyby to działało również w innych przeglądarkach.

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

Zaletą tego podejścia jest to, że nie musisz modyfikować kodu, który zamierzasz przetestować.

Conrad Calmez
źródło
Właśnie zauważyłem, że to nie zadziała w PhantomJS 2.1.1. ;)
Conrad Calmez
4

Nie musisz przekazywać obiektu pamięci do każdej metody, która go używa. Zamiast tego można użyć parametru konfiguracyjnego dla dowolnego modułu, który dotyka adaptera pamięci masowej.

Twój stary moduł

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

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

Twój nowy moduł z funkcją „wrapper” config

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

Kiedy używasz modułu w testowaniu kodu

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

MockStorageKlasa może wyglądać następująco

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

Gdy używasz modułu w kodzie produkcyjnym, zamiast tego przekaż prawdziwy adapter localStorage

const myModule = require('./my-module')(window.localStorage)
Dziękuję Ci
źródło
fyi do ludzi, jest to ważne tylko w es6: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… (ale jest to świetne rozwiązanie i nie mogę czekać, aż będzie dostępne wszędzie!)
Alex Moore- Niemi
@ AlexMoore-Niemi ES6 jest tutaj bardzo mało przydatne. Wszystko to można by zrobić używając ES5 lub niższej z bardzo niewielkimi zmianami.
Dziękuję
tak, wystarczy wskazać export default functioni zainicjować moduł za pomocą takiego argumentu jak es6. wzór stoi niezależnie.
Alex Moore-Niemi
Co? Musiałem użyć starszego stylu, requireaby zaimportować moduł i zastosować go do argumentu w tym samym wyrażeniu. Nie ma sposobu, aby to zrobić w ES6, o którym wiem. W przeciwnym razie użyłbym ES6import
dziękuję
2

Postanowiłem powtórzyć mój komentarz do odpowiedzi Pumby80 jako oddzielnej odpowiedzi, aby łatwiej było ponownie wykorzystać ją jako bibliotekę.

Wziąłem kod Pumba80, nieco go dopracowałem, dodałem testy i opublikowałem jako moduł npm tutaj: https://www.npmjs.com/package/mock-local-storage .

Oto kod źródłowy: https://github.com/letsrock-today/mock-local-storage/blob/master/src/mock-localstorage.js

Niektóre testy: https://github.com/letsrock-today/mock-local-storage/blob/master/test/mock-localstorage.js

Moduł tworzy mock localStorage i sessionStorage na obiekcie globalnym (window lub global, który z nich jest zdefiniowany).

W testach mojego innego projektu wymagałem tego z mokką w ten sposób: mocha -r mock-local-storageaby globalne definicje były dostępne dla całego testowanego kodu.

Zasadniczo kod wygląda następująco:

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

Zwróć uwagę, że wszystkie metody dodane za pośrednictwem, Object.definePropertyaby nie były iterowane, otwierane ani usuwane jako zwykłe elementy i nie były liczone w długości. Dodałem również sposób rejestrowania wywołania zwrotnego, które jest wywoływane, gdy przedmiot ma zostać umieszczony w obiekcie. To wywołanie zwrotne może służyć do emulacji błędu przekroczenia limitu w testach.

nikolay_turpitko
źródło
2

Okazało się, że nie muszę z tego kpić. Mogłem zmienić rzeczywistą lokalną pamięć masową na stan, przez który chciałem setItem, a następnie po prostu zapytać o wartości, aby sprawdzić, czy zmieniły się za pośrednictwem getItem. To nie jest tak potężne, jak kpiny, ponieważ nie widać, ile razy coś zostało zmienione, ale zadziałało dla moich celów.

RandomEngy
źródło
0

Niestety, jedynym sposobem, w jaki możemy mockować obiekt localStorage w scenariuszu testowym, jest zmiana testowanego kodu. Musisz opakować swój kod w anonimową funkcję (co i tak powinieneś zrobić) i użyć „iniekcji zależności”, aby przekazać referencję do obiektu window. Coś jak:

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

Następnie w trakcie testu możesz określić:

window.mockWindow = { localStorage: { ... } };
John Kurlak
źródło
0

Tak lubię to robić. To proste.

  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
źródło
0

kredyty dla https://medium.com/@armno/til-mocking-localstorage-and-sessionstorage-in-angular-unit-tests-a765abdc9d87 Zrób fałszywy lokalny magazyn i szpieguj w lokalnym magazynie, gdy jest potrzebny

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

I tutaj go używamy

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

    expect(...
  });
Johansrk
źródło