Как имитировать 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();

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


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.

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


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


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(...
  });
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.