Pregunta ¿Cómo burlarse localStorage en las pruebas de unidades de JavaScript?


¿Hay alguna biblioteca para burlarse? localStorage?

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

Mi prueba inicial muestra que localStorage se niega a ser asignable en firefox (sadface) así que probablemente necesite algún tipo de truco sobre esto: /

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

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

¿Qué piensas de estos enfoques y crees que hay otras formas mejores de hacerlo? De cualquier manera, pondré la "biblioteca" resultante que termino haciendo en github por bondad de fuente abierta.


75
2017-07-14 16:24


origen


Respuestas:


Aquí hay una manera simple 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 quiere burlarse del almacenamiento local en todas sus pruebas, declare el beforeEach() función que se muestra arriba en el alcance global de sus pruebas (el lugar habitual es un specHelper.js guión).


102
2018-01-17 15:04



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

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

Y luego lo que realmente haces, es algo como eso:

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

38
2017-10-03 11:07



También considere la opción de inyectar dependencias en la función de constructor 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)

En línea con la burla y las pruebas unitarias, me gusta evitar probar la implementación de 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 los métodos en el objeto localStorage real, use un archivo simulado "mudo" y resuelva los métodos individuales como desee, como por ejemplo:

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

17
2017-11-22 20:04



¿Hay alguna biblioteca 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 el contexto global. Con una función de envoltura como la anterior, funciona bien.


7
2017-07-14 18:34



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

6
2018-03-03 10:11



Aquí hay un ejemplo usando sinon espía y simulacro:

// 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
2018-03-02 14:12



No tiene que pasar el objeto de almacenamiento a cada método que lo usa. En su lugar, puede usar 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 config "wrapper"

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

Cuando usas el módulo en el 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')
})

los MockStorage clase 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 de almacenamiento local real

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

4
2017-11-07 19:03



Sobrescribir el localStorage propiedad de la global window objeto como se sugiere en algunas de las respuestas no funcionará en la mayoría de los motores JS, ya que declaran el localStorage propiedad de datos como no grabable y no configurable.

Sin embargo, descubrí que al menos con la versión de WebKit de PhantomJS (versión 1.9.8) podría usar la API heredada. __defineGetter__ para controlar lo que sucede si localStorage es accedido Aún así sería interesante si esto funciona en otros navegadores también.

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.


2
2018-02-10 15:18



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

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

Luego, dentro de su prueba, puede especificar:

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

1
2018-03-06 21:30



Decidí reiterar mi comentario a la respuesta de Pumbaa80 como respuesta por separado para que sea más fácil reutilizarlo como una 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 localStorage falso y sessionStorage 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 esta: mocha -r mock-local-storage para hacer que las definiciones globales estén disponibles para todos los códigos bajo prueba.

Básicamente, el código se ve de la siguiente manera:

(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 añadidos a través de Object.defineProperty para que no se repitan, accedan o eliminen como elementos regulares 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 ser puesto en el objeto. Esta devolución de llamada se puede usar para emular el error de exceso de cuota en las pruebas.


1
2018-04-14 19:19