web-dev-qa-db-fra.com

Comment se moquer de LocalStorage dans les tests unitaires JavaScript?

Existe-t-il des bibliothèques pour simuler localStorage?

J'utilise Sinon.JS pour la plupart de mes autres javascript moqueurs et je trouve que c'est vraiment génial.

Mes tests initiaux montrent que localStorage refuse d’être assignable dans firefox (sadface), il me faudra donc probablement un bidouillage autour de ceci: /

Mes options pour l'instant (comme je vois) sont les suivantes:

  1. Créer des fonctions d'habillage que tout mon code utilise et les imiter
  2. Créez une sorte de gestion d’états (peut-être compliquée) (instantané localStorage avant le test, dans l’instantané de restauration du nettoyage) pour localStorage.
  3. ??????

Que pensez-vous de ces approches et pensez-vous qu'il existe d'autres meilleurs moyens de s'y prendre? De toute façon, je mettrai la "bibliothèque" résultante que je finirai par créer sur github pour la bonté open source.

81
Anthony Sottile

Voici un moyen simple de s'en moquer avec 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 = {};
  });
});

Si vous souhaitez simuler le stockage local dans tous vos tests, déclarez la fonction beforeEach() indiquée ci-dessus dans l'étendue globale de vos tests (l'emplacement habituel est un script specHelper.js).

108
Andreas Köberle

simulez simplement le localStorage/sessionStorage global (ils ont la même API) pour vos besoins.
Par exemple:

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

Et puis ce que vous faites réellement est quelque chose comme ça:

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

Pensez également à l'option permettant d'injecter des dépendances dans la fonction constructeur d'un objet.

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)

En phase avec les moqueries et les tests unitaires, j'aime éviter de tester la mise en œuvre du stockage. Par exemple, il est inutile de vérifier si la durée de stockage a augmenté après la définition d'un élément, etc. 

Comme il n’est évidemment pas fiable de remplacer des méthodes sur l’objet réel localStorage, utilisez un mockStorage "stupide" et modifiez les méthodes individuelles à votre guise, telles que:

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

C'est ce que je fais...

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

Existe-t-il des bibliothèques pour simuler localStorage?

Je viens d'écrire un:

(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

})();

Mes tests initiaux montrent que localStorage refuse d’être assignable dans Firefox

Seulement dans un contexte global. Avec une fonction wrapper comme ci-dessus, cela fonctionne très bien.

6
user123444555621

Voici un exemple utilisant 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
Manuel Bitto

Remplacer la propriété localStorage de l'objet global window tel que suggéré dans certaines des réponses ne fonctionnera pas dans la plupart des moteurs JS, car ils déclarent que la propriété de données localStorage n'est ni inscriptible ni configurable.

Cependant, j'ai découvert qu'au moins avec la version WebKit de PhantomJS (version 1.9.8), vous pouviez utiliser l'API héritée __defineGetter__ pour contrôler ce qui se passe si vous accédez à localStorage. Néanmoins, il serait intéressant que cela fonctionne également dans d’autres navigateurs.

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

L'avantage de cette approche est que vous n'auriez pas à modifier le code que vous êtes sur le point de tester.

4
Conrad Calmez

Les solutions actuelles ne fonctionneront pas dans Firefox. En effet, localStorage est défini par la spécification html comme non modifiable. Vous pouvez toutefois contourner ce problème en accédant directement au prototype de localStorage.

La solution inter-navigateurs consiste à simuler les objets sur Storage.prototype, par exemple.

au lieu de spyOn (localStorage, 'setItem'), utilisez 

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

extrait des réponses de bzbarsky et teogeos ici https://github.com/jasmine/jasmine/issues/299

3
roo2

Il n'est pas nécessaire de transmettre l'objet de stockage à chaque méthode qui l'utilise. À la place, vous pouvez utiliser un paramètre de configuration pour tout module touchant l'adaptateur de stockage.

Votre ancien module

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

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

Votre nouveau module avec la fonction "wrapper" de config

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

Lorsque vous utilisez le module dans le code de test

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

La classe MockStorage pourrait ressembler à ceci

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

Lorsque vous utilisez votre module dans le code de production, transmettez plutôt le véritable adaptateur localStorage

const myModule = require('./my-module')(window.localStorage)
3
user633183

J'ai décidé de réitérer mon commentaire à la réponse de Pumbaa80 en tant que réponse séparée afin qu'il soit plus facile de le réutiliser en tant que bibliothèque.

J'ai pris le code de Pumbaa80, je l'ai affiné un peu, ajouté des tests et publié sous forme de module npm ici: https://www.npmjs.com/package/mock-local-storage .

Voici un code source: https://github.com/letsrock-today/mock-local-storage/blob/master/src/mock-localstorage.js

Quelques tests: https://github.com/letsrock-today/mock-local-storage/blob/master/test/mock-localstorage.js

Le module crée un mock localStorage et une sessionStorage sur l'objet global (fenêtre ou global, lequel d'entre eux est défini).

Dans les tests de mon autre projet, je l’avais demandé avec mocha comme ceci: mocha -r mock-local-storage pour rendre les définitions globales disponibles pour tout le code testé.

Fondamentalement, le code ressemble à ce qui suit:

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

Notez que toutes les méthodes ajoutées via Object.defineProperty afin qu'elles ne soient pas itérées, consultées ou supprimées en tant qu'éléments standard et ne comptent pas en longueur. J'ai également ajouté un moyen d'enregistrer le rappel qui est appelé lorsqu'un article est sur le point d'être placé dans un objet. Ce rappel peut être utilisé pour émuler une erreur dépassant le quota dans les tests.

1
nikolay_turpitko

C'est comme ça que j'aime le faire. Reste simple.

  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

J'ai trouvé que je n'avais pas besoin de m'en moquer. Je pouvais changer le stockage local réel dans l'état où je le voulais via setItem, puis interroger simplement les valeurs pour voir s'il était modifié via getItem. Ce n'est pas aussi puissant que de vous moquer, car vous ne pouvez pas voir combien de fois quelque chose a changé, mais cela a fonctionné pour moi.

0
RandomEngy

Malheureusement, la seule façon de simuler l'objet localStorage dans un scénario de test est de modifier le code que nous testons. Vous devez envelopper votre code dans une fonction anonyme (ce que vous devriez faire de toute façon) et utiliser "injection de dépendance" pour transmettre une référence à l'objet window. Quelque chose comme:

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

Ensuite, dans votre test, vous pouvez spécifier:

window.mockWindow = { localStorage: { ... } };
0
John Kurlak