Comment se moquer de localStorage dans les tests unitaires JavaScript?


103

Y a-t-il des bibliothèques à se moquer localStorage?

J'ai utilisé Sinon.JS pour la plupart de mes autres moqueries javascript et j'ai trouvé que c'était vraiment génial.

Mes tests initiaux montrent que localStorage refuse d'être assignable dans Firefox (sadface) donc j'aurai probablement besoin d'une sorte de piratage autour de ceci: /

Mes options pour le moment (comme je le vois) sont les suivantes:

  1. Créer des fonctions d'emballage que tout mon code utilise et se moquer de celles-ci
  2. Créez une sorte de gestion de l'état (qui peut être compliquée) (instantané localStorage avant le test, dans le nettoyage de l'instantané de restauration) pour localStorage.
  3. ??????

Que pensez-vous de ces approches et pensez-vous qu'il existe d'autres moyens plus efficaces pour y parvenir? Quoi qu'il en soit, je mettrai la "bibliothèque" résultante que je finis par créer sur github pour la bonté open source.


34
Vous avez manqué # 4:Profit!
Chris Laplante

Réponses:


128

Voici un moyen simple de se moquer de 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 vous moquer du stockage local dans tous vos tests, déclarez la beforeEach()fonction ci-dessus dans la portée globale de vos tests (l'emplacement habituel est un script specHelper.js ).


1
+1 - vous pouvez également le faire avec sinon. La clé est pourquoi il faut se moquer de tout l'objet localStorage, il suffit de se moquer des méthodes (getItem et / ou setItem) qui vous intéressent.
s1mm0t

6
Attention: il semble y avoir un problème avec cette solution dans Firefox: github.com/pivotal/jasmine/issues/299
cthulhu

4
J'obtiens un ReferenceError: localStorage is not defined(en cours d'exécution des tests en utilisant FB Jest et npm)… des idées comment contourner?
FeifanZ

1
Essayez d'espionnerwindow.localStorage
Benj

22
andCallFakechangé and.callFakeen jasmin 2. +
Venugopal

51

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

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

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

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

1
Modifier suggestion: getItemdoit retourner nulllorsque la valeur n'existe pas: return storage[key] || null;;
cyberwombat

8
À partir de 2016, il semble que cela ne fonctionne pas dans les navigateurs modernes (vérifiés Chrome et Firefox); le dépassement localStoragedans son ensemble n'est pas possible.
jakub.g

2
Ouais, malheureusement, cela ne fonctionne plus, mais je dirais aussi que storage[key] || nullc'est incorrect. S'il storage[key] === 0reviendra à la nullplace. Je pense que vous pourriez le faire return key in storage ? storage[key] : null.
redbmk

Je viens de l'utiliser sur SO! Fonctionne comme un charme - il suffit de changer localStor pour revenir à localStorage sur un vrai serveurfunction 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 J'obtiens une erreur après la mise à jour du nœud vers 10.15.1 TypeError: Cannot set property localStorage of #<Window> which has only a getter, une idée comment puis-je résoudre ce problème?
Tasawer Nawaz

19

Envisagez également la possibilité 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)

Dans la lignée des moqueries et des tests unitaires, j'aime éviter de tester l'implémentation du stockage. Par exemple, inutile de vérifier si la durée de stockage a augmenté après avoir défini un élément, etc.

Puisqu'il n'est évidemment pas fiable de remplacer les méthodes sur le véritable objet localStorage, utilisez un mockStorage "stupide" et stub les méthodes individuelles comme vous le souhaitez, comme:

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
Je me rends compte que cela fait un moment que je n'ai pas examiné cette question - mais c'est en fait ce que j'ai fini par faire.
Anthony Sottile

1
C'est la seule solution valable, car elle ne présente pas un risque aussi élevé de rupture dans le temps.
oligofren

14

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

12

Les solutions actuelles ne fonctionneront pas dans Firefox. C'est parce que localStorage est défini par la spécification html comme n'étant pas modifiable. Vous pouvez cependant contourner ce problème en accédant directement au prototype de localStorage.

La solution de navigateur croisé consiste à se moquer des objets sur Storage.prototypepar 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


1
Votre commentaire devrait recevoir plus de likes. Je vous remercie!
LorisBachert

6

Y a-t-il des bibliothèques à se moquer localStorage?

Je viens d'en é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 premiers tests montrent que localStorage refuse d'être assignable dans Firefox

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


1
vous pouvez également utiliservar window = { localStorage: ... }
user123444555621

1
Malheureusement, cela signifie que j'aurais besoin de connaître toutes les propriétés dont j'aurais besoin et que j'aurais ajouté à l'objet window (et je rate son prototype, etc.). Y compris tout ce dont jQuery peut avoir besoin. Malheureusement, cela semble être une non-solution. Oh aussi, les tests testent du code qui utilise localStorage, les tests n'ont pas nécessairement localStoragedirectement en eux. Cette solution ne modifie pas le localStoragepour les autres scripts, c'est donc une non-solution. +1 pour le tour de recherche
Anthony Sottile

1
Vous devrez peut-être adapter votre code pour le rendre testable. Je sais que c'est très ennuyeux, et c'est pourquoi je préfère les tests de sélénium lourds aux tests unitaires.
user123444555621

Ce n'est pas une solution valable. Si vous appelez une fonction à partir de cette fonction anonyme, vous perdrez la référence à la fenêtre fictive ou à l'objet localStorage simulé. Le but d'un test unitaire est d'appeler une fonction extérieure. Ainsi, lorsque vous appelez votre fonction qui fonctionne avec localStorage, elle n'utilisera pas la simulation. Au lieu de cela, vous devez envelopper le code que vous testez dans une fonction anonyme. Pour le rendre testable, faites-lui accepter l'objet window comme paramètre.
John Kurlak

Ce simulacre a un bogue: lors de la récupération d'un élément qui n'existe pas, getItem doit retourner null. Dans le simulacre, il renvoie undefined. Le code correct devrait êtreif this.hasOwnProperty(key) return this[key] else return null
Evan

4

Voici un exemple utilisant sinon spy et 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

L'écrasement de la localStoragepropriété de l' windowobjet global comme suggéré dans certaines des réponses ne fonctionnera pas dans la plupart des moteurs JS, car ils déclarent la localStoragepropriété data comme non accessible en écriture et non configurable.

Cependant, j'ai découvert qu'au moins avec la version WebKit de PhantomJS (version 1.9.8), vous pouviez utiliser l'ancienne API __defineGetter__pour contrôler ce qui se passe en cas d' localStorageaccès. 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.


Je viens de remarquer que cela ne fonctionnera pas dans PhantomJS 2.1.1. ;)
Conrad Calmez

4

Vous n'êtes pas obligé de transmettre l'objet de stockage à chaque méthode qui l'utilise. Au lieu de cela, vous pouvez utiliser un paramètre de configuration pour tout module qui touche 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 de configuration "wrapper"

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

Lorsque vous utilisez le module pour tester le code

// 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 MockStorageclasse 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, passez plutôt l'adaptateur de stockage local réel

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

Pour info, ceci n'est valable que dans es6: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/... (mais c'est une excellente solution et j'ai hâte qu'elle soit disponible partout!)
Alex Moore- Niemi

@ AlexMoore-Niemi il y a très peu d'utilisation d'ES6 ici. Tout cela pourrait être fait en utilisant ES5 ou une version inférieure avec très peu de changements.
Merci

oui, il suffit de signaler export default functionet d'initialiser un module avec un argument comme celui-ci est uniquement es6. le motif se tient indépendamment.
Alex Moore-Niemi

Hein? J'ai dû utiliser l'ancien style requirepour importer un module et l'appliquer à un argument dans la même expression. Il n'y a aucun moyen de faire cela dans ES6 que je sache. Sinon, j'aurais utilisé ES6import
Merci

2

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

J'ai pris le code de Pumbaa80, je l'ai raffiné un peu, j'ai ajouté des tests et je l'ai publié en tant que 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 sessionStorage sur l'objet global (window ou global, lequel d'entre eux est défini).

Dans les tests de mon autre projet, je l'ai exigé avec mocha comme ceci: mocha -r mock-local-storagepour rendre les définitions globales disponibles pour tout le code sous test.

Fondamentalement, le code ressemble à ceci:

(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.definePropertyafin qu'elles ne soient pas répétées, consultées ou supprimées en tant qu'éléments normaux et ne comptent pas dans leur longueur. J'ai également ajouté un moyen d'enregistrer le rappel qui est appelé lorsqu'un élément est sur le point d'être placé dans l'objet. Ce rappel peut être utilisé pour émuler une erreur de dépassement de quota dans les tests.


2

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


0

Malheureusement, la seule façon de se moquer de 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épendances" pour passer une référence à l'objet window. Quelque chose comme:

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

Ensuite, à l'intérieur de votre test, vous pouvez spécifier:

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

0

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

crédits à https://medium.com/@armno/til-mocking-localstorage-and-sessionstorage-in-angular-unit-tests-a765abdc9d87 Faites un faux stockage local, et espionnez le stockage local, quand il est appelé

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

Et ici on l'utilise

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

    expect(...
  });
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.