Як знущатися з локального зберігання в тестах блоку JavaScript?


103

Чи є там бібліотеки, щоб знущатися localStorage?

Я використовую Sinon.JS для більшості моїх інших глузувань JavaScript, і я виявив, що це справді чудово.

Моє первісне тестування показує, що localStorage відмовляється призначатись у firefox (sadface), тому мені, певно, знадобиться якийсь злом навколо цього: /

На сьогоднішній день мої варіанти (як я бачу) такі:

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

Як ви думаєте про ці підходи та чи вважаєте ви, що є інші кращі способи досягти цього? У будь-якому випадку я покладу отриману "бібліотеку", яку я в кінцевому підсумку буду створювати на github для відкритого коду.


34
Ви пропустили №4:Profit!
Кріс Лаплант

Відповіді:


128

Ось простий спосіб знущатися з Жасмін:

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 - ви могли б це зробити і з синоном. Ключовим є те, чому турбуєтеся знущатися над усім об’єктом локального сховища, просто знущайтеся з методів (getItem та / або setItem), які вас цікавлять.
s1mm0t

6
Голова вгору: Начебто, проблема з цим рішенням у Firefox: github.com/pivotal/jasmine/isissue/299
cthulhu

4
Я отримую ReferenceError: localStorage is not defined(запущені тести, використовуючи FB Jest та npm)… будь-які ідеї, як обійтися ?
FeifanZ

1
Спробуйте шпигуватиwindow.localStorage
Benj

21
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

Просто використовував це на SO! Працює як шарм - просто потрібно змінити 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
Я усвідомлюю, що минуло певний час, коли я переглянув це питання - але це насправді те, що я закінчив робити.
Ентоні Соттіл

1
Це єдине гідне рішення, оскільки воно не має такого високого ризику зламатись в часі.
олігофрен

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

12

Поточні рішення не працюватимуть у Firefox. Це тому, що localStorage визначається специфікацією html як такий, що не може бути змінений. Однак ви зможете обійти це, звернувшись безпосередньо до прототипу localStorage.

Рішення перехресного браузера - це знущання над об'єктами, Storage.prototypeнаприклад

замість spyOn (localStorage, 'setItem') використовуйте

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

взяті з відповідей bzbarsky та teogeos тут https://github.com/jasmine/jasmine/isissue/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 для оглядового трюк , хоча
Ентоні Sottile

1
Можливо, вам доведеться адаптувати свій код, щоб зробити його перевіреним. Я знаю, що це дуже дратує, і тому я віддаю перевагу важким тестуванням селену перед одиничними тестами.
користувач123444555621

Це неправильне рішення. Якщо ви зателефонуєте до будь-якої функції з цієї анонімної функції, ви втратите посилання на макетне вікно або знущання з об’єкта localStorage. Метою одиничного тесту є те, що ви НЕ викликаєте зовнішню функцію. Отже, коли ви викликаєте функцію, яка працює з localStorage, вона не використовуватиме макет. Натомість вам потрібно загорнути тестований код в анонімну функцію. Щоб зробити його тестуваним, нехай він приймає об’єкт вікна як параметр.
Джон Курлак

У цьому макеті є помилка. Під час отримання елемента, який не існує, getItem повинен повернути null. У макеті він повертається невизначеним. Правильний код повинен бутиif this.hasOwnProperty(key) return this[key] else return null
Еван

4

Ось приклад використання синона шпигуна та знущань:

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

Ваш новий модуль з функцією конфігурації "обгортка"

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/… (але це чудове рішення, і я не можу чекати, поки його буде доступно всюди!)
Алекс Мур- Ніємі

@ 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

Модуль створює макет 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.