¿Cómo simular localStorage en pruebas unitarias de JavaScript?

103

¿Hay bibliotecas para burlarse localStorage?

He estado usando Sinon.JS para la mayoría de mis otras simulaciones de JavaScript y he descubierto que es realmente genial.

Mi prueba inicial muestra que localStorage se niega a ser asignable en firefox (cara triste), por lo que probablemente necesite algún tipo de truco para esto: /

Mis opciones a partir de ahora (como veo) son las siguientes:

  1. Crear funciones de envoltura que usa todo mi código y simularlas
  2. Cree algún tipo de administración estatal (podría ser complicado) (instantánea localStorage antes de la prueba, en instantánea de restauración de limpieza) para localStorage.
  3. ??????

¿Qué opinas de estos enfoques y crees que hay otras formas mejores de hacerlo? De cualquier manera, pondré la "biblioteca" resultante que termino haciendo en github para la bondad del código abierto.

Anthony Sottile
fuente
34
Te perdiste # 4:Profit!
Chris Laplante

Respuestas:

128

Aquí hay una forma sencilla de burlarse de ella con 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 desea simular el almacenamiento local en todas sus pruebas, declare la beforeEach()función que se muestra arriba en el alcance global de sus pruebas (el lugar habitual es un script specHelper.js ).

Andreas Köberle
fuente
1
+1 - también podrías hacer esto con sinon. La clave es por qué molestarse en burlarse de todo el objeto localStorage, simplemente burlarse de los métodos (getItem y / o setItem) que le interesan.
s1mm0t
6
Aviso: parece haber un problema con esta solución en Firefox: github.com/pivotal/jasmine/issues/299
cthulhu
4
Me sale un ReferenceError: localStorage is not defined(ejecutando pruebas usando FB Jest y npm)… ¿alguna idea de cómo solucionarlo?
FeifanZ
1
Intenta espiarwindow.localStorage
Benj
21
andCallFakecambiado a and.callFakeen jazmín 2. +
Venugopal
51

simplemente simule el localStorage / sessionStorage global (tienen la misma API) para sus necesidades.
Por ejemplo:

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

Y luego, lo que realmente haces es algo así:

// mock the localStorage
window.localStorage = storageMock();
// mock the sessionStorage
window.sessionStorage = storageMock();
a8m
fuente
1
Editar sugerencia: getItemdebe regresar nullcuando el valor no existe return storage[key] || null;:;
cyberwombat
8
A partir de 2016, parece que esto no funciona en los navegadores modernos (comprobado Chrome y Firefox); localStorageno es posible anular en su conjunto.
jakub.g
2
Sí, desafortunadamente esto ya no funciona, pero también diría que storage[key] || nulles incorrecto. Si storage[key] === 0volverá en su nulllugar. Aunque creo que podrías hacerlo return key in storage ? storage[key] : null.
redbmk
¡Solo usé esto en SO! Funciona como un encanto, solo tiene que cambiar localStor de nuevo a localStorage cuando esté en un servidor realfunction 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 Recibo un error después de actualizar el nodo a 10.15.1 TypeError: Cannot set property localStorage of #<Window> which has only a getter, ¿alguna idea de cómo puedo solucionarlo?
Tasawer Nawaz
19

También considere la opción de inyectar dependencias en la función constructora de un objeto.

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)

De acuerdo con las simulaciones y las pruebas unitarias, me gusta evitar probar la implementación del almacenamiento. Por ejemplo, no tiene sentido verificar si la duración del almacenamiento aumentó después de configurar un elemento, etc.

Dado que obviamente no es confiable reemplazar métodos en el objeto localStorage real, use un mockStorage "tonto" y apunte los métodos individuales como desee, como:

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');
Claudijo
fuente
1
Me doy cuenta de que ha pasado un tiempo desde que miré esta pregunta, pero esto es lo que terminé haciendo.
Anthony Sottile
1
Esta es la única solución que vale la pena, ya que no tiene un riesgo tan alto de romperse en el tiempo.
oligofren
14

Esto es lo que hago...

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

Las soluciones actuales no funcionarán en Firefox. Esto se debe a que localStorage está definido por la especificación html como no modificable. Sin embargo, puede evitar esto accediendo directamente al prototipo de localStorage.

La solución de navegador cruzado es burlarse de los objetos en Storage.prototype por ejemplo,

en lugar de spyOn (localStorage, 'setItem') use

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

tomado de las respuestas de bzbarsky y teogeos aquí https://github.com/jasmine/jasmine/issues/299

roo2
fuente
1
Tu comentario debería recibir más Me gusta. ¡Gracias!
LorisBachert
6

¿Hay bibliotecas para burlarse localStorage?

Acabo de escribir uno:

(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

})();

Mi prueba inicial muestra que localStorage se niega a ser asignable en Firefox

Solo en contexto global. Con una función de envoltura como la anterior, funciona bien.

usuario123444555621
fuente
1
también puede usarvar window = { localStorage: ... }
user123444555621
1
Desafortunadamente, eso significa que necesitaría conocer todas las propiedades que necesitaré y que agregaré al objeto de ventana (y me pierdo su prototipo, etc.). Incluyendo todo lo que jQuery pueda necesitar. Desafortunadamente, esto parece una no solución. Oh, también, las pruebas son código de prueba que usa localStorage, las pruebas no necesariamente tienen localStoragedirectamente en ellas. Esta solución no cambia la localStoragede otros scripts, por lo que no es una solución. Sin embargo
Anthony Sottile
1
Es posible que deba adaptar su código para que sea comprobable. Sé que esto es muy molesto y por eso prefiero las pruebas de selenio intensas a las pruebas unitarias.
user123444555621
Ésta no es una solución válida. Si llama a cualquier función desde esa función anónima, perderá la referencia a la ventana simulada o al objeto localStorage simulado. El propósito de una prueba unitaria es llamar a una función externa. Entonces, cuando llame a su función que funciona con localStorage, no usará el simulacro. En su lugar, debe envolver el código que está probando en una función anónima. Para que sea comprobable, haga que acepte el objeto de ventana como parámetro.
John Kurlak
Ese simulacro tiene un error: al recuperar un elemento que no existe, getItem debería devolver un valor nulo. En el simulacro, devuelve indefinido. El código correcto debería serif this.hasOwnProperty(key) return this[key] else return null
Evan
4

Aquí hay un ejemplo usando 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();
Manuel Bitto
fuente
4

Sobrescribir la localStoragepropiedad del windowobjeto global como se sugiere en algunas de las respuestas no funcionará en la mayoría de los motores JS, porque declaran que la localStoragepropiedad de datos no se puede escribir y no se puede configurar.

Sin embargo, descubrí que al menos con la versión de WebKit de PhantomJS (versión 1.9.8) se puede usar la API heredada __defineGetter__para controlar lo que sucede si localStoragese accede. Aún así, sería interesante si esto también funciona en otros navegadores.

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

El beneficio de este enfoque es que no tendría que modificar el código que está a punto de probar.

Conrad Calmez
fuente
Acabo de notar que esto no funcionará en PhantomJS 2.1.1. ;)
Conrad Calmez
4

No es necesario que pase el objeto de almacenamiento a cada método que lo utilice. En su lugar, puede utilizar un parámetro de configuración para cualquier módulo que toque el adaptador de almacenamiento.

Tu antiguo módulo

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

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

Su nuevo módulo con la función "envoltorio" de configuración

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

Cuando usa el módulo en código de prueba

// 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 MockStorageclase podría verse así

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

Cuando use su módulo en el código de producción, en su lugar pase el adaptador localStorage real

const myModule = require('./my-module')(window.localStorage)
Gracias
fuente
Fyi para la gente, esto solo es válido en es6: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… (¡pero es una gran solución y no puedo esperar hasta que esté disponible en todas partes!)
Alex Moore- Niemi
@ AlexMoore-Niemi, aquí se usa muy poco ES6. Todo esto se puede hacer usando ES5 o versiones anteriores con muy pocos cambios.
Gracias
sí, solo señalando export default function e inicializar un módulo con un argumento como ese es solo es6. el patrón se mantiene independientemente.
Alex Moore-Niemi
¿Eh? Tuve que usar el estilo anterior requirepara importar un módulo y aplicarlo a un argumento en la misma expresión. No hay forma de hacer eso en ES6 que yo sepa. De lo contrario, habría usado ES6import
Gracias
2

Decidí reiterar mi comentario a la respuesta de Pumbaa80 como una respuesta separada para que sea más fácil reutilizarlo como biblioteca.

Tomé el código de Pumbaa80, lo refiné un poco, agregué pruebas y lo publiqué como un módulo npm aquí: https://www.npmjs.com/package/mock-local-storage .

Aquí hay un código fuente: https://github.com/letsrock-today/mock-local-storage/blob/master/src/mock-localstorage.js

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

El módulo crea simulacro de almacenamiento local y almacenamiento de sesión en el objeto global (ventana o global, cuál de ellos está definido).

En las pruebas de mi otro proyecto lo requería con mocha como este: mocha -r mock-local-storage hacer que las definiciones globales estén disponibles para todo el código bajo prueba.

Básicamente, el código tiene el siguiente aspecto:

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

Tenga en cuenta que todos los métodos agregados mediante Object.definePropertypara que no se repitan, accedan o eliminen como elementos normales y no cuenten en longitud. También agregué una forma de registrar la devolución de llamada que se llama cuando un elemento está a punto de colocarse en un objeto. Esta devolución de llamada se puede utilizar para emular el error de superación de la cuota en las pruebas.

nikolay_turpitko
fuente
2

Descubrí que no necesitaba burlarme de él. Podría cambiar el almacenamiento local real al estado en el que lo quería setItem, luego simplemente consultar los valores para ver si cambió a través de getItem. No es tan poderoso como burlón, ya que no puedes ver cuántas veces se cambió algo, pero funcionó para mis propósitos.

RandomEngy
fuente
0

Desafortunadamente, la única forma en que podemos burlarnos del objeto localStorage en un escenario de prueba es cambiar el código que estamos probando. Tienes que envolver tu código en una función anónima (que deberías estar haciendo de todos modos) y usar "inyección de dependencia" para pasar una referencia al objeto de la ventana. Algo como:

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

Luego, dentro de su prueba, puede especificar:

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

Así es como me gusta hacerlo. Lo mantiene 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;
      });

    });
  });
Eduardo La Hoz Miranda
fuente
0

créditos a https://medium.com/@armno/til-mocking-localstorage-and-sessionstorage-in-angular-unit-tests-a765abdc9d87 Haga un almacenamiento local falso y espíe el almacenamiento local, cuando sea caleld

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

Y aqui lo usamos

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

    expect(...
  });
Johansrk
fuente