Como faço para lidar com o localStorage em testes de brincadeira?

144

Continuo recebendo "localStorage não está definido" nos testes do Jest, o que faz sentido, mas quais são minhas opções? Bater nas paredes de tijolos.

Chiedo
fonte

Respostas:

141

Ótima solução de @chiedo

No entanto, usamos a sintaxe do ES2015 e achei que era um pouco mais limpo escrevê-lo dessa maneira.

class LocalStorageMock {
  constructor() {
    this.store = {};
  }

  clear() {
    this.store = {};
  }

  getItem(key) {
    return this.store[key] || null;
  }

  setItem(key, value) {
    this.store[key] = value.toString();
  }

  removeItem(key) {
    delete this.store[key];
  }
};

global.localStorage = new LocalStorageMock;
nickcan
fonte
8
Provavelmente deve fazer value + ''na setter como nulo alça e valores indefinidos corretamente
menehune23
Eu acho que a última brincadeira estava apenas usando, || nullé por isso que meu teste estava falhando, porque no meu teste eu estava usando not.toBeDefined(). @Chiedo solução fazê-lo funcionar novamente
jcubic
Eu acho que isso é tecnicamente um esboço :) veja aqui a versão ridicularizada: stackoverflow.com/questions/32911630/…
TigerBear
100

Descobri isso com a ajuda do seguinte: https://groups.google.com/forum/#!topic/jestjs/9EPhuNWVYTg

Configure um arquivo com o seguinte conteúdo:

var localStorageMock = (function() {
  var store = {};
  return {
    getItem: function(key) {
      return store[key];
    },
    setItem: function(key, value) {
      store[key] = value.toString();
    },
    clear: function() {
      store = {};
    },
    removeItem: function(key) {
      delete store[key];
    }
  };
})();
Object.defineProperty(window, 'localStorage', { value: localStorageMock });

Então você adiciona a seguinte linha ao seu package.json nas configurações do Jest

"setupTestFrameworkScriptFile":"PATH_TO_YOUR_FILE",

Chiedo
fonte
6
Aparentemente, com uma das atualizações, o nome desse parâmetro foi alterado e agora é chamado "setupTestFrameworkScriptFile"
Grzegorz Pawlik
2
"setupFiles": [...]funciona também. Com a opção de matriz, permite separar simulações em arquivos separados. Por exemplo:"setupFiles": ["<rootDir>/__mocks__/localStorageMock.js"]
Stiggler 23/03
3
O valor de retorno getItemdifere um pouco do que seria retornado por um navegador se nenhum dado for definido em uma chave específica. chamar getItem("foo")quando não está definido retornará, por exemplo, nullem um navegador, mas undefinedcom essa simulação - isso estava causando a falha de um dos meus testes. Solução simples para mim era voltar store[key] || nullna getItemfunção
Ben Broadley
isso não funciona se você fizer algo parecido com:localStorage['test'] = '123'; localStorage.getItem('test')
rob
3
Estou recebendo o seguinte erro - o valor jest.fn () deve ser uma função ou espião falso. Alguma ideia?
Paul Fitzgerald
55

Se estiver usando create-react-app, há uma solução mais simples e direta explicada na documentação .

Crie src/setupTests.jse coloque isso nele:

const localStorageMock = {
  getItem: jest.fn(),
  setItem: jest.fn(),
  clear: jest.fn()
};
global.localStorage = localStorageMock;

Tom Mertz contribuição em um comentário abaixo:

Em seguida, você pode testar se as funções do seu localStorageMock são usadas fazendo algo como

expect(localStorage.getItem).toBeCalledWith('token')
// or
expect(localStorage.getItem.mock.calls.length).toBe(1)

dentro de seus testes, se você quiser ter certeza de que foi chamado. Confira https://facebook.github.io/jest/docs/en/mock-functions.html

c4k
fonte
Oi c4k! Você poderia dar um exemplo de como você usaria isso em seus testes?
Dimo
O que você quer dizer ? Você não precisa inicializar nada nos seus testes, apenas zomba automaticamente do que localStoragevocê usa no seu código. (se você usa create-react-appe todos os scripts automáticos que fornece naturalmente)
c4k
Em seguida, você pode testar se as funções do localStorageMock são usadas executando algo como expect(localStorage.getItem).toBeCalledWith('token')ou expect(localStorage.getItem.mock.calls.length).toBe(1)dentro dos testes, se você quiser ter certeza de que foi chamado. Confira facebook.github.io/jest/docs/en/mock-functions.html
Tom Mertz 06/06/19
10
por isso estou recebendo um erro - o valor jest.fn () deve ser uma função ou espião falso. Alguma ideia?
Paul Fitzgerald
3
Isso não causará problemas se você tiver vários testes em uso localStorage? Você não gostaria de redefinir os espiões após cada teste para evitar a "propagação" em outros testes?
Brandon Sturgeon
43

Atualmente (outubro de 19), o localStorage não pode ser ridicularizado ou espionado pelo gracejo, como você faria normalmente, e conforme descrito nos documentos de criação e reação de aplicativos. Isso ocorre devido a alterações feitas no jsdom. Você pode ler sobre isso nos rastreadores de brincadeiras e jsdom .

Como solução alternativa, você pode espionar o protótipo:

// does not work:
jest.spyOn(localStorage, "setItem");
localStorage.setItem = jest.fn();

// works:
jest.spyOn(window.localStorage.__proto__, 'setItem');
window.localStorage.__proto__.setItem = jest.fn();

// assertions as usual:
expect(localStorage.setItem).toHaveBeenCalled();
Bastian Stein
fonte
Na verdade, ele funciona para mim apenas com o spyOn, não há necessidade de substituir a função setItemjest.spyOn(window.localStorage.__proto__, 'setItem');
Yohan Dahmani
Sim, listei os dois como alternativas, não há necessidade de fazer os dois.
Bastian Stein
Eu quis dizer sem a substituição do setItem também 😉
Yohan Dahmani
Eu acho que não entendo. Você pode esclarecer por favor?
Bastian Stein
1
Ah sim. Eu estava dizendo que você pode usar a primeira linha ou a segunda linha. São alternativas que fazem a mesma coisa. Seja qual for a sua preferência pessoal :) Desculpe a confusão.
Bastian Stein
13

Uma alternativa melhor que lida com undefinedvalores (não possui toString()) e retorna nullse o valor não existir. Testei isso com a reactv15 reduxeredux-auth-wrapper

class LocalStorageMock {
  constructor() {
    this.store = {}
  }

  clear() {
    this.store = {}
  }

  getItem(key) {
    return this.store[key] || null
  }

  setItem(key, value) {
    this.store[key] = value
  }

  removeItem(key) {
    delete this.store[key]
  }
}

global.localStorage = new LocalStorageMock
Dmitriy
fonte
Agradeço a Alexis Tyler pela ideia de adicionar removeItem: developer.mozilla.org/pt-BR/docs/Web/API/Storage/removeItem
Dmitriy
Acreditam nula e necessidade indefinido para resultar em "nulo" e "indefinido" (strings literais)
menehune23
6

Se você está procurando um mock e não um stub, aqui está a solução que eu uso:

export const localStorageMock = {
   getItem: jest.fn().mockImplementation(key => localStorageItems[key]),
   setItem: jest.fn().mockImplementation((key, value) => {
       localStorageItems[key] = value;
   }),
   clear: jest.fn().mockImplementation(() => {
       localStorageItems = {};
   }),
   removeItem: jest.fn().mockImplementation((key) => {
       localStorageItems[key] = undefined;
   }),
};

export let localStorageItems = {}; // eslint-disable-line import/no-mutable-exports

Exporto os itens de armazenamento para facilitar a inicialização. IE eu posso facilmente configurá-lo para um objeto

Nas versões mais recentes do Jest + JSDom, não é possível definir isso, mas o armazenamento local já está disponível e você pode espioná-lo da seguinte forma:

const setItemSpy = jest.spyOn(Object.getPrototypeOf(window.localStorage), 'setItem');
TigerBear
fonte
5

Encontrei esta solução no github

var localStorageMock = (function() {
  var store = {};

  return {
    getItem: function(key) {
        return store[key] || null;
    },
    setItem: function(key, value) {
        store[key] = value.toString();
    },
    clear: function() {
        store = {};
    }
  }; 
})();

Object.defineProperty(window, 'localStorage', {
 value: localStorageMock
});

Você pode inserir esse código em seus setupTests e deve funcionar bem.

Eu testei em um projeto com tip typtipt.

Carlos Huamani
fonte
para mim, Object.defineProperty fez o truque. A atribuição direta de objetos não funcionou. Obrigado!
Vicens Fayos
4

Infelizmente, as soluções que encontrei aqui não funcionaram para mim.

Então, eu estava olhando para os problemas do Jest GitHub e encontrei este tópico

As soluções mais votadas foram as seguintes:

const spy = jest.spyOn(Storage.prototype, 'setItem');

// or

Storage.prototype.getItem = jest.fn(() => 'bla');
Christian Saiki
fonte
Meus testes também não têm windowou Storagedefiniram. Talvez seja a versão mais antiga do Jest que estou usando.
Antrikshy
3

Como @ ck4, a documentação sugerida tem uma explicação clara para o uso localStorageem tom de brincadeira. No entanto, as funções simuladas falharam ao executar qualquer um dos localStoragemétodos.

Abaixo está o exemplo detalhado do meu componente react, que faz uso de métodos abstratos para escrever e ler dados,

//file: storage.js
const key = 'ABC';
export function readFromStore (){
    return JSON.parse(localStorage.getItem(key));
}
export function saveToStore (value) {
    localStorage.setItem(key, JSON.stringify(value));
}

export default { readFromStore, saveToStore };

Erro:

TypeError: _setupLocalStorage2.default.setItem is not a function

Fix:
Adicionar abaixo função simulada para jest (caminho: .jest/mocks/setUpStore.js)

let mockStorage = {};

module.exports = window.localStorage = {
  setItem: (key, val) => Object.assign(mockStorage, {[key]: val}),
  getItem: (key) => mockStorage[key],
  clear: () => mockStorage = {}
};

O snippet é referenciado a partir daqui

Mad-D
fonte
2

Apresentamos algumas outras respostas aqui para resolvê-lo em um projeto com o Typecript. Eu criei um LocalStorageMock assim:

export class LocalStorageMock {

    private store = {}

    clear() {
        this.store = {}
    }

    getItem(key: string) {
        return this.store[key] || null
    }

    setItem(key: string, value: string) {
        this.store[key] = value
    }

    removeItem(key: string) {
        delete this.store[key]
    }
}

Em seguida, criei uma classe LocalStorageWrapper que eu uso para todo o acesso ao armazenamento local no aplicativo em vez de acessar diretamente a variável de armazenamento local global. Facilitou a configuração da simulação no wrapper para testes.

CorayThan
fonte
2
    describe('getToken', () => {
    const Auth = new AuthService();
    const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Ik1yIEpvc2VwaCIsImlkIjoiNWQwYjk1Mzg2NTVhOTQ0ZjA0NjE5ZTA5IiwiZW1haWwiOiJ0cmV2X2pvc0Bob3RtYWlsLmNvbSIsInByb2ZpbGVVc2VybmFtZSI6Ii9tcmpvc2VwaCIsInByb2ZpbGVJbWFnZSI6Ii9Eb3Nlbi10LUdpci1sb29rLWN1dGUtbnVrZWNhdDMxNnMtMzExNzAwNDYtMTI4MC04MDAuanBnIiwiaWF0IjoxNTYyMzE4NDA0LCJleHAiOjE1OTM4NzYwMDR9.YwU15SqHMh1nO51eSa0YsOK-YLlaCx6ijceOKhZfQZc';
    beforeEach(() => {
        global.localStorage = jest.fn().mockImplementation(() => {
            return {
                getItem: jest.fn().mockReturnValue(token)
            }
        });
    });
    it('should get the token from localStorage', () => {

        const result  = Auth.getToken();
        expect(result).toEqual(token);

    });
});

Crie uma simulação e adicione-a ao globalobjeto

Trevor Joseph
fonte
2

Você pode usar essa abordagem para evitar zombarias.

Storage.prototype.getItem = jest.fn(() => expectedPayload);
Sanath
fonte
2

Você precisa simular o armazenamento local com esses trechos

// localStorage.js

var localStorageMock = (function() {
    var store = {};

    return {
        getItem: function(key) {
            return store[key] || null;
        },
        setItem: function(key, value) {
            store[key] = value.toString();
        },
        clear: function() {
            store = {};
        }
    };

})();

Object.defineProperty(window, 'localStorage', {
     value: localStorageMock
});

E na configuração de brincadeira:

"setupFiles":["localStorage.js"]

Sinta-se livre para perguntar qualquer coisa.

Slim Coder
fonte
1

A solução a seguir é compatível para testes com configurações mais rigorosas do TypeScript, ESLint, TSLint e Prettier { "proseWrap": "always", "semi": false, "singleQuote": true, "trailingComma": "es5" }:

class LocalStorageMock {
  public store: {
    [key: string]: string
  }
  constructor() {
    this.store = {}
  }

  public clear() {
    this.store = {}
  }

  public getItem(key: string) {
    return this.store[key] || undefined
  }

  public setItem(key: string, value: string) {
    this.store[key] = value.toString()
  }

  public removeItem(key: string) {
    delete this.store[key]
  }
}
/* tslint:disable-next-line:no-any */
;(global as any).localStorage = new LocalStorageMock()

HT / https://stackoverflow.com/a/51583401/101290 para saber como atualizar global.localStorage

Beau Smith
fonte
1

Para fazer o mesmo no texto datilografado, faça o seguinte:

Configure um arquivo com o seguinte conteúdo:

let localStorageMock = (function() {
  let store = new Map()
  return {

    getItem(key: string):string {
      return store.get(key);
    },

    setItem: function(key: string, value: string) {
      store.set(key, value);
    },

    clear: function() {
      store = new Map();
    },

    removeItem: function(key: string) {
        store.delete(key)
    }
  };
})();
Object.defineProperty(window, 'localStorage', { value: localStorageMock });

Então você adiciona a seguinte linha ao seu package.json nas configurações do Jest

"setupTestFrameworkScriptFile":"PATH_TO_YOUR_FILE",

Ou você importa esse arquivo no seu caso de teste em que deseja simular o armazenamento local.

vs_lala
fonte
0

Isso funcionou para mim,

delete global.localStorage;
global.localStorage = {
getItem: () => 
 }
prático
fonte