Как имитировать localStorage в модульных тестах JavaScript?

104

Есть ли какие-нибудь библиотеки, которые можно поиздеваться localStorage?

Я использую Sinon.JS для большинства других своих издевательств над javascript и обнаружил, что это действительно здорово.

Мое первоначальное тестирование показывает, что localStorage не может быть назначен в firefox (sadface), поэтому мне, вероятно, понадобится какой-то взлом по этому поводу: /

Мои варианты на данный момент (как я вижу) следующие:

  1. Создавайте функции обертывания, которые использует весь мой код, и имитируйте те
  2. Создайте какое-то (может быть сложно) управление состоянием (снимок localStorage перед тестом, снимок восстановления после очистки) для localStorage.
  3. ??????

Что вы думаете об этих подходах, и как вы думаете, есть ли другие способы сделать это лучше? В любом случае я помещу получившуюся «библиотеку», которую в конечном итоге сделаю, на github для добра с открытым кодом.

Энтони Соттиле
источник
35
Вы пропустили №4:Profit!
Крис Лапланте,

Ответы:

129

Вот простой способ поиздеваться над этим с помощью Жасмин:

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

Если вы хотите имитировать локальное хранилище во всех ваших тестах, объявите beforeEach()функцию, показанную выше, в глобальной области ваших тестов (обычно это скрипт specHelper.js ).

Андреас Кёберле
источник
1
+1 - вы можете сделать это и с sinon. Ключ в том, зачем беспокоиться о том, чтобы издеваться над всем объектом localStorage, просто имитируйте методы (getItem и / или setItem), которые вас интересуют.
s1mm0t 05
6
Внимание: похоже, есть проблема с этим решением в Firefox: github.com/pivotal/jasmine/issues/299
cthulhu
4
Я получаю ReferenceError: localStorage is not defined(запуск тестов с использованием FB Jest и npm)… есть идеи, как обойтись ?
FeifanZ
1
Попробуйте шпионить заwindow.localStorage
Бендж
22
andCallFakeизменен на and.callFakeжасмин 2. +
Venugopal
51

просто имитируйте глобальный localStorage / sessionStorage (у них одинаковый API) для ваших нужд.
Например:

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

И тогда вы на самом деле делаете что-то вроде этого:

// mock the localStorage
window.localStorage = storageMock();
// mock the sessionStorage
window.sessionStorage = storageMock();
a8m
источник
1
Предложение по редактированию: getItemдолжно возвращаться, nullесли значение не существует return storage[key] || null;:;
cyberwombat
8
По состоянию на 2016 год, похоже, это не работает в современных браузерах (проверено Chrome и Firefox); переопределение localStorageв целом невозможно.
jakub.g
2
Да, к сожалению, это больше не работает, но я бы сказал, что storage[key] || nullэто неверно. Если storage[key] === 0он вернется nullвместо этого. Я думаю, ты return key in storage ? storage[key] : nullсправишься.
redbmk
Просто использовал это на ТАК! Работает как шарм - просто нужно изменить localStor обратно на localStorage на реальном сервере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
2
@ a8m Я получаю сообщение об ошибке после обновления узла до 10.15.1 TypeError: Cannot set property localStorage of #<Window> which has only a getter, есть идеи, как это исправить?
Tasawer Nawaz
19

Также рассмотрите возможность внедрения зависимостей в функцию конструктора объекта.

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)

В соответствии с имитацией и модульным тестированием мне нравится избегать тестирования реализации хранилища. Например, нет смысла проверять, увеличилась ли длина хранилища после того, как вы установили элемент и т. Д.

Поскольку очевидно, что замена методов в реальном объекте localStorage ненадежна, используйте «тупой» mockStorage и при необходимости вставьте отдельные методы в заглушки, например:

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
Я понимаю, что прошло много времени с тех пор, как я смотрел на этот вопрос, но на самом деле я этим и закончил.
Anthony Sottile
1
Это единственное стоящее решение, так как у него не такой высокий риск поломки во времени.
oligofren
14

Вот что я делаю...

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,
});
ЧакДжарди
источник
13

Текущие решения не будут работать в Firefox. Это связано с тем, что localStorage определяется спецификацией html как не подлежащий изменению. Однако вы можете обойти это, обратившись к прототипу localStorage напрямую.

Кроссбраузерное решение - издеваться над объектами, Storage.prototypeнапример,

вместо spyOn (localStorage, 'setItem') используйте

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

взято из ответов bzbarsky и teogeos здесь https://github.com/jasmine/jasmine/issues/299

roo2
источник
1
Ваш комментарий должен получить больше лайков. Спасибо!
LorisBachert
6

Есть ли какие-нибудь библиотеки, чтобы высмеивать localStorage?

Я только что написал:

(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

})();

Мое первоначальное тестирование показывает, что localStorage отказывается назначаться в firefox.

Только в глобальном контексте. С функцией-оболочкой, как указано выше, она работает нормально.

user123444555621
источник
1
Вы также можете использоватьvar window = { localStorage: ... }
user123444555621
1
К сожалению, это означает, что мне нужно знать все свойства, которые мне понадобятся и которые я добавил к объекту окна (и я упускаю его прототип и т. Д.). Включая все, что может понадобиться jQuery. К сожалению, это не решение. Кроме того, тесты - это тестовый код, который использует localStorage, тесты не обязательно содержат localStorageнепосредственно в них. Это решение не меняет localStorageдругих сценариев, поэтому это не решение. +1 за трюк с
обзором
1
Возможно, вам придется адаптировать свой код, чтобы сделать его тестируемым. Я знаю, что это очень раздражает, и поэтому я предпочитаю тяжелое тестирование селена модульным тестам.
user123444555621
Это недопустимое решение. Если вы вызовете любую функцию из этой анонимной функции, вы потеряете ссылку на фиктивное окно или фиктивный объект localStorage. Цель модульного теста состоит в том, чтобы вы ДЕЙСТВИТЕЛЬНО вызывали внешнюю функцию. Поэтому, когда вы вызываете свою функцию, которая работает с localStorage, она не будет использовать макет. Вместо этого вам нужно заключить тестируемый код в анонимную функцию. Чтобы сделать его тестируемым, пусть он принимает объект окна в качестве параметра.
Джон Курлак
В этом макете есть ошибка: при получении несуществующего элемента getItem должен возвращать null. В макете он возвращает undefined. Правильный код должен бытьif this.hasOwnProperty(key) return this[key] else return null
Эван
4

Вот пример использования 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();
Мануэль Битто
источник
4

Перезапись localStorageсвойства глобального windowобъекта, как предлагается в некоторых ответах, не будет работать в большинстве движков JS, потому что они объявляютlocalStorage свойство данных как недоступное для записи и настройки.

Однако я обнаружил, что, по крайней мере, с версией WebKit PhantomJS (версия 1.9.8) вы можете использовать устаревший API __defineGetter__для управления тем, что происходит, если localStorageк нему обращаются. Тем не менее было бы интересно, если бы это работало и в других браузерах.

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

Преимущество этого подхода в том, что вам не придется изменять код, который вы собираетесь тестировать.

Конрад Кальмез
источник
Только заметил, что это не будет работать в PhantomJS 2.1.1. ;)
Конрад Кальмез
4

Вам не нужно передавать объект хранилища каждому методу, который его использует. Вместо этого вы можете использовать параметр конфигурации для любого модуля, который касается адаптера хранилища.

Ваш старый модуль

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

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

Ваш новый модуль с функцией config "wrapper"

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

Когда вы используете модуль в тестовом коде

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

MockStorageКласс может выглядеть следующим образом

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

При использовании вашего модуля в производственном коде вместо этого передайте реальный адаптер localStorage

const myModule = require('./my-module')(window.localStorage)
Спасибо
источник
к вашему сведению, это действительно только в es6: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… (но это отличное решение, и я не могу дождаться, пока оно будет доступно повсюду!)
Alex Moore- Niemi
@ AlexMoore-Niemi здесь очень мало использования ES6. Все это можно сделать с помощью ES5 или ниже с очень небольшими изменениями.
Спасибо
да, просто указание export default functionи инициализация модуля с таким аргументом - это только es6. шаблон остается неизменным.
Алекс Мур-Ниеми
А? Мне пришлось использовать старый стиль, requireчтобы импортировать модуль и применить его к аргументу в том же выражении. Я не знаю, как сделать это в ES6. В противном случае я бы использовал ES6import
Спасибо
2

Я решил повторить свой комментарий к ответу Pumbaa80 как отдельный ответ, чтобы его было легче повторно использовать в качестве библиотеки.

Я взял код Pumbaa80, немного доработал его, добавил тесты и опубликовал его как модуль npm здесь: https://www.npmjs.com/package/mock-local-storage .

Вот исходный код: https://github.com/letsrock-today/mock-local-storage/blob/master/src/mock-localstorage.js

Некоторые тесты: https://github.com/letsrock-today/mock-local-storage/blob/master/test/mock-localstorage.js

Модуль создает mock localStorage и sessionStorage на глобальном объекте (оконном или глобальном, какой из них определен).

В тестах другого моего проекта я требовал этого с мокко следующим образом: mocha -r mock-local-storageсделать глобальные определения доступными для всего тестируемого кода.

В основном код выглядит следующим образом:

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

Обратите внимание, что все методы добавлены через, Object.definePropertyпоэтому они не будут повторяться, использоваться или удаляться как обычные элементы и не учитываются по длине. Также я добавил способ регистрации обратного вызова, который вызывается, когда элемент собирается быть помещенным в объект. Этот обратный вызов может использоваться для имитации ошибки превышения квоты в тестах.

николай турпитько
источник
2

Я обнаружил, что мне не нужно издеваться над этим. Я мог бы изменить фактическое локальное хранилище на состояние, которое я хотел setItem, а затем просто запросить значения, чтобы увидеть, изменилось ли оно через getItem. Это не так мощно, как насмешка, поскольку вы не можете увидеть, сколько раз что-то менялось, но для моих целей это сработало.

RandomEngy
источник
0

К сожалению, единственный способ имитировать объект localStorage в тестовом сценарии - это изменить тестируемый код. Вы должны заключить свой код в анонимную функцию (что вы все равно должны делать) и использовать «внедрение зависимостей» для передачи ссылки на объект окна. Что-то вроде:

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

Затем внутри вашего теста вы можете указать:

window.mockWindow = { localStorage: { ... } };
Джон Курлак
источник
0

Вот как я люблю это делать. Все просто.

  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

кредиты на https://medium.com/@armno/til-mocking-localstorage-and-sessionstorage-in-angular-unit-tests-a765abdc9d87 Сделайте поддельное локальное хранилище и шпионите за локальным хранилищем, когда оно настроено

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

И здесь мы его используем

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

    expect(...
  });
Johansrk
источник