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

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

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

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

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

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

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

Ответ 1

Вот простой способ издеваться над 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 = {};
  });
});

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

Ответ 2

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

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

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

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

Ответ 3

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

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

Ответ 4

Это то, что я делаю...

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

Ответ 5

Есть ли там какие-нибудь библиотеки, чтобы издеваться над 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

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

Ответ 6

Вот пример использования sinon spy и 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();

Ответ 7

Перезапись свойства 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 });

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

Ответ 8

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

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

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

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

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

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)

Ответ 9

Текущие решения не будут работать в 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

Ответ 10

Я решил повторить свой комментарий к ответу 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: 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, чтобы они не были итерированы, доступны или удалены как обычные элементы и не будут учитываться в длине. Также я добавил способ зарегистрировать обратный вызов, который вызывается, когда предмет собирается быть помещен в объект. Этот обратный вызов может использоваться для эмуляции превышения квоты в тестах.

Ответ 11

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

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

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

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

Ответ 12

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

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

    });
  });

Ответ 13

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