Como simular localStorage em testes de unidade de JavaScript?

103

Existe alguma biblioteca para zombar localStorage?

Tenho usado o Sinon.JS para a maioria das minhas outras simulações de javascript e descobri que é realmente ótimo.

Meu teste inicial mostra que localStorage se recusa a ser atribuível no firefox (sadface), então provavelmente vou precisar de algum tipo de hack em torno disso: /

Minhas opções a partir de agora (como vejo) são as seguintes:

  1. Criar funções de empacotamento que todo meu código usa e simular aquelas
  2. Crie algum tipo de (pode ser complicado) gerenciamento de estado (instantâneo localStorage antes do teste, no instantâneo de restauração de limpeza) para localStorage.
  3. ??????

O que você acha dessas abordagens e acha que há outras maneiras melhores de fazer isso? De qualquer forma, colocarei a "biblioteca" resultante que acabo criando no github para as vantagens do código aberto.

Anthony Sottile
fonte
34
Você perdeu # 4:Profit!
Chris Laplante

Respostas:

128

Aqui está uma maneira simples de zombar com 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 = {};
  });
});

Se você quiser simular o armazenamento local em todos os seus testes, declare a beforeEach()função mostrada acima no escopo global de seus testes (o local usual é um script specHelper.js ).

Andreas Köberle
fonte
1
+1 - você pode fazer isso com o sinon também. A chave é por que se preocupar em simular todo o objeto localStorage, apenas simular os métodos (getItem e / ou setItem) em que você está interessado.
s1mm0t
6
Atenção: parece haver um problema com esta solução no Firefox: github.com/pivotal/jasmine/issues/299
cthulhu
4
Eu recebo um ReferenceError: localStorage is not defined(testes de execução usando FB Jest e npm) ... alguma idéia de como contornar?
FeifanZ
1
Tente espionarwindow.localStorage
Benj
21
andCallFakealterado para and.callFakeem jasmim 2. +
Venugopal
51

apenas simule o localStorage / sessionStorage global (eles têm a mesma API) para suas necessidades.
Por exemplo:

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

E então o que você realmente faz é algo assim:

// mock the localStorage
window.localStorage = storageMock();
// mock the sessionStorage
window.sessionStorage = storageMock();
a8m
fonte
1
Sugestão de edição: getItemdeve retornar nullquando o valor não existir return storage[key] || null;:;
cyberwombat
8
Em 2016, parece que isso não funciona em navegadores modernos (verificado Chrome e Firefox); substituir localStoragecomo um todo não é possível.
jakub.g
2
Sim, infelizmente isso não funciona mais, mas também eu diria que storage[key] || nullestá incorreto. Se storage[key] === 0ele voltará em seu nulllugar. Eu acho que você poderia fazer, no return key in storage ? storage[key] : nullentanto.
redbmk
Acabei de usar isso no SO! Funciona perfeitamente - basta alterar o localStor de volta para localStorage quando estiver em um 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 Estou recebendo um erro após atualizar o nó para 10.15.1 que TypeError: Cannot set property localStorage of #<Window> which has only a getter, alguma idéia de como posso corrigir isso?
Tasawer Nawaz de
19

Considere também a opção de injetar dependências na função construtora de um 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)

Em linha com a simulação e o teste de unidade, gosto de evitar testar a implementação do armazenamento. Por exemplo, não adianta verificar se o comprimento de armazenamento aumentou depois de definir um item, etc.

Visto que obviamente não é confiável substituir métodos no objeto localStorage real, use um mockStorage "burro" e faça stub dos métodos individuais conforme desejado, 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
fonte
1
Sei que já faz um tempo que não vejo essa pergunta - mas na verdade foi isso que acabei fazendo.
Anthony Sottile
1
Esta é a única solução que vale a pena, pois não apresenta um risco tão elevado de quebra a tempo.
oligofren
14

Isto é o que eu faço...

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
fonte
12

As soluções atuais não funcionarão no Firefox. Isso ocorre porque localStorage é definido pela especificação html como não sendo modificável. No entanto, você pode contornar isso acessando o protótipo de localStorage diretamente.

A solução para vários navegadores é simular os objetos em, Storage.prototypepor exemplo

em vez de spyOn (localStorage, 'setItem') use

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

retirado das respostas de bzbarsky e teogeos aqui https://github.com/jasmine/jasmine/issues/299

roo2
fonte
1
Seu comentário deve receber mais curtidas. Obrigado!
LorisBachert
6

Existe alguma biblioteca para zombar localStorage?

Acabei de escrever um:

(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

})();

Meu teste inicial mostra que localStorage se recusa a ser atribuível no firefox

Apenas no contexto global. Com uma função de invólucro como acima, funciona perfeitamente.

user123444555621
fonte
1
você também pode usarvar window = { localStorage: ... }
user123444555621
1
Infelizmente, isso significa que eu precisaria saber todas as propriedades de que preciso e adicionei ao objeto de janela (e sinto falta de seu protótipo, etc.). Incluindo tudo o que o jQuery possa precisar. Infelizmente, isso parece não ser uma solução. Ah também, os testes são códigos de teste que usam localStorage, os testes não necessariamente têm localStoragediretamente neles. Esta solução não altera o localStoragepara outros scripts, portanto, é uma não solução. 1 para o truque do escopo
Anthony Sottile
1
Você pode precisar adaptar seu código para torná-lo testável. Sei que isso é muito chato e é por isso que prefiro testes intensos de selênio a testes de unidade.
user123444555621
Esta não é uma solução válida. Se você chamar qualquer função de dentro dessa função anônima, perderá a referência à janela fictícia ou ao objeto localStorage fictício. O objetivo de um teste de unidade é que você chame uma função externa. Portanto, quando você chamar sua função que funciona com localStorage, ela não usará o mock. Em vez disso, você deve envolver o código que está testando em uma função anônima. Para torná-lo testável, aceite o objeto de janela como um parâmetro.
John Kurlak
Esse mock tem um bug: ao recuperar um item que não existe, getItem deve retornar null. Na simulação, ele retorna indefinido. O código correto deve serif this.hasOwnProperty(key) return this[key] else return null
Evan
4

Aqui está um exemplo usando sinon spy e 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
fonte
4

Substituir a localStoragepropriedade do windowobjeto global, conforme sugerido em algumas das respostas, não funcionará na maioria dos mecanismos JS, porque eles declaram a localStoragepropriedade de dados como não gravável e não configurável.

No entanto, descobri que pelo menos com a versão do WebKit do PhantomJS (versão 1.9.8) você poderia usar a API legada __defineGetter__para controlar o que acontece se localStoragefor acessado. Ainda assim, seria interessante se isso funcionasse em outros navegadores também.

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

O benefício dessa abordagem é que você não precisa modificar o código que está prestes a testar.

Conrad Calmez
fonte
Notei que isso não funcionará no PhantomJS 2.1.1. ;)
Conrad Calmez
4

Você não precisa passar o objeto de armazenamento para cada método que o utiliza. Em vez disso, você pode usar um parâmetro de configuração para qualquer módulo que toque o adaptador de armazenamento.

Seu módulo antigo

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

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

Seu novo módulo com função de configuração de "wrapper"

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

Quando você usa o módulo no código de teste

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

A MockStorageclasse pode ser assim

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

Ao usar seu módulo no código de produção, em vez disso, passe o adaptador localStorage real

const myModule = require('./my-module')(window.localStorage)
Obrigado
fonte
Para falar a verdade, isso só é válido em es6: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… (mas é uma ótima solução e não posso esperar até que esteja disponível em todos os lugares!)
Alex Moore- Niemi
@ AlexMoore-Niemi há muito pouco uso de ES6 aqui. Tudo isso pode ser feito usando ES5 ou inferior com muito poucas alterações.
Obrigado
sim, apenas apontar export default functione inicializar um módulo com um argumento como esse é apenas es6. o padrão permanece independentemente.
Alex Moore-Niemi
Hã? Tive que usar o estilo antigo requirepara importar um módulo e aplicá-lo a um argumento na mesma expressão. Não há como fazer isso no ES6 que eu conheça. Caso contrário, eu teria usado o ES6import
Obrigado
2

Decidi reiterar meu comentário à resposta de Pumbaa80 como uma resposta separada para que seja mais fácil reutilizá-la como uma biblioteca.

Peguei o código do Pumbaa80, refinei um pouco, adicionei testes e publiquei como um módulo npm aqui: https://www.npmjs.com/package/mock-local-storage .

Aqui está um código-fonte: https://github.com/letsrock-today/mock-local-storage/blob/master/src/mock-localstorage.js

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

O módulo cria localStorage e sessionStorage simulados no objeto global (janela ou global, qual deles é definido).

Em meus outros testes de projeto, eu o solicitei com o mocha da seguinte forma: mocha -r mock-local-storagepara disponibilizar definições globais para todo o código em teste.

Basicamente, o código se parece com o seguinte:

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

Observe que todos os métodos adicionados por meio de Object.definePropertypara que não sejam iterados, acessados ​​ou removidos como itens regulares e não contam em comprimento. Também adicionei uma maneira de registrar o retorno de chamada que é chamado quando um item está prestes a ser colocado no objeto. Este retorno de chamada pode ser usado para emular o erro de cota excedida em testes.

nikolay_turpitko
fonte
2

Descobri que não precisava zombar disso. Eu poderia alterar o armazenamento local real para o estado que eu queria por meio de setItem, em seguida, apenas consultar os valores para ver se ele mudou via getItem. Não é tão poderoso quanto zombeteiro, pois você não consegue ver quantas vezes algo foi alterado, mas funcionou para meus propósitos.

RandomEngy
fonte
0

Infelizmente, a única maneira de simular o objeto localStorage em um cenário de teste é alterar o código que estamos testando. Você tem que envolver seu código em uma função anônima (o que você deve fazer de qualquer maneira) e usar "injeção de dependência" para passar uma referência ao objeto de janela. Algo como:

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

Então, dentro do seu teste, você pode especificar:

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

É assim que gosto de fazer. Mantém tudo simples.

  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
fonte
0

créditos para https://medium.com/@armno/til-mocking-localstorage-and-sessionstorage-in-angular-unit-tests-a765abdc9d87 Faça um armazenamento local falso e espie o armazenamento local, quando for calado

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

E aqui nós usamos

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

    expect(...
  });
Johansrk
fonte