Jak radzić sobie z testami localStorage?


144

Ciągle otrzymuję komunikat „localStorage nie jest zdefiniowany” w testach Jest, co ma sens, ale jakie mam opcje? Uderzanie w ceglane ściany.

Odpowiedzi:


141

Świetne rozwiązanie od @chiedo

Jednak używamy składni ES2015 i czułem, że pisanie tego w ten sposób było trochę czystsze.

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;

8
Powinien prawdopodobnie zrobić value + ''w
seterze, aby

Myślę, że ten ostatni żart po prostu używał tego || null, dlatego mój test się nie powiódł, ponieważ w moim teście używałem not.toBeDefined(). Rozwiązanie @Chiedo sprawi, że znowu zadziała
jcubic

Myślę, że technicznie jest to niedopałek :) Zobacz tutaj, aby zobaczyć wyśmiewaną wersję: stackoverflow.com/questions/32911630/ ...
TigerBear

100

Rozgryzłem to z pomocą: https://groups.google.com/forum/#!topic/jestjs/9EPhuNWVYTg

Skonfiguruj plik o następującej zawartości:

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

Następnie dodaj następujący wiersz do pliku package.json w ramach konfiguracji Jest

"setupTestFrameworkScriptFile":"PATH_TO_YOUR_FILE",


6
Najwyraźniej przy jednej z aktualizacji zmieniła się nazwa tego parametru i teraz nazywa się "setupTestFrameworkScriptFile"
Grzegorz Pawlik

2
"setupFiles": [...]działa również. Z opcją array pozwala na rozdzielenie mocków na osobne pliki. Np .:"setupFiles": ["<rootDir>/__mocks__/localStorageMock.js"]
Stiggler

3
Zwracana wartość getItemróżni się nieznacznie od wartości zwracanej przez przeglądarkę, jeśli brak danych zostanie ustawiony dla określonego klucza. wywołanie, getItem("foo")gdy nie jest ustawione, na przykład zwróci nullw przeglądarce, ale undefinedprzez ten próbny - to powodowało niepowodzenie jednego z moich testów. Prostym rozwiązaniem dla mnie był powrót store[key] || nullw getItemfunkcji
Ben Broadley

to nie działa, jeśli zrobisz coś takiegolocalStorage['test'] = '123'; localStorage.getItem('test')
okradnij

3
Otrzymuję następujący błąd - wartość jest.fn () musi być funkcją pozorowaną lub szpiegowską. Jakieś pomysły?
Paul Fitzgerald

55

W przypadku korzystania z aplikacji create-react-app istnieje prostsze i bardziej zrozumiałe rozwiązanie opisane w dokumentacji .

Utwórz src/setupTests.jsi umieść w nim:

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

Wkład Toma Mertza w komentarzu poniżej:

Następnie możesz sprawdzić, czy funkcje localStorageMock są używane, wykonując coś takiego jak

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

jeśli chcesz się upewnić, że został wywołany. Sprawdź https://facebook.github.io/jest/docs/en/mock-functions.html


Cześć c4k! Czy mógłbyś podać przykład, jak użyłbyś tego w swoich testach?
Dimo

Co masz na myśli ? Nie musisz niczego inicjować w swoich testach, po prostu automatycznie mockuje to, localStorageczego używasz w swoim kodzie. (jeśli używasz create-react-appi wszystkich automatycznych skryptów, które zapewnia naturalnie)
c4k

Następnie możesz sprawdzić, czy funkcje localStorageMock są używane, wykonując coś podobnego expect(localStorage.getItem).toBeCalledWith('token')lub expect(localStorage.getItem.mock.calls.length).toBe(1)wewnątrz testów, jeśli chcesz się upewnić, że został wywołany. Zajrzyj na facebook.github.io/jest/docs/en/mock-functions.html
Tom Mertz

10
w tym celu otrzymuję błąd - wartość jest.fn () musi być funkcją pozorowaną lub szpiegowską. Jakieś pomysły?
Paul Fitzgerald

3
Czy nie spowoduje to problemów, jeśli masz wiele testów, które używają localStorage? Czy nie chciałbyś zresetować szpiegów po każdym teście, aby zapobiec „przenikaniu” do innych testów?
Brandon Sturgeon

43

Obecnie (październik '19) localStorage nie można wyszydzać ani szpiegować żartem, jak to zwykle bywa, jak opisano w dokumentach aplikacji create-react-app. Wynika to ze zmian wprowadzonych w jsdom. Możesz o tym przeczytać w żartach i jsdom issue trackers.

Aby obejść ten problem, możesz zamiast tego szpiegować prototyp:

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

Właściwie to działa dla mnie tylko ze spyOn, nie ma potrzeby nadpisywania funkcji setItemjest.spyOn(window.localStorage.__proto__, 'setItem');
Yohan Dahmani

Tak, wymieniłem oba jako alternatywy, nie ma potrzeby robić obu.
Bastian Stein

miałem na myśli również bez nadpisywania setItem 😉
Yohan Dahmani

Chyba nie rozumiem. Czy możesz wyjaśnić, proszę?
Bastian Stein

1
O tak. Mówiłem, że możesz użyć pierwszej lub drugiej linii. Są alternatywami, które robią to samo. Jakiekolwiek jest twoje osobiste preferencje :) Przepraszam za zamieszanie.
Bastian Stein


13

Lepsza alternatywa, która obsługuje undefinedwartości (których nie ma toString()) i zwraca, nulljeśli wartość nie istnieje. Przetestowano to w reactwersji 15 reduxiredux-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

Podziękowania dla Alexis Tyler za pomysł dodania removeItem: developer.mozilla.org/en-US/docs/Web/API/Storage/removeItem
Dmitriy

Wierzą, nieważne i nieokreśloną potrzebę spowodować „null” i „nieokreślone” (dosłowne ciągi)
menehune23

6

Jeśli szukasz makiety a nie stubu, oto rozwiązanie, którego używam:

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

Eksportuję elementy pamięci w celu łatwej inicjalizacji. IE Mogę łatwo ustawić go na obiekt

W nowszych wersjach Jest + JSDom nie można tego ustawić, ale lokalna pamięć jest już dostępna i można ją szpiegować w następujący sposób:

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

5

Znalazłem to rozwiązanie z 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
});

Możesz wstawić ten kod w swoim setupTests i powinien działać dobrze.

Przetestowałem to w projekcie z typem.


dla mnie Object.defineProperty zrobił sztuczkę. Bezpośrednie przypisanie obiektu nie działa. Dzięki!
Vicens Fayos

4

Niestety rozwiązania, które tu znalazłem, nie zadziałały.

Więc szukałem problemów z Jest GitHub i znalazłem ten wątek

Najbardziej pozytywnymi rozwiązaniami były te:

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

// or

Storage.prototype.getItem = jest.fn(() => 'bla');

Moje testy też nie mają windowlub nie są Storagezdefiniowane. Może to starsza wersja Jest, której używam.
Antrikshy

3

Jak sugerował @ ck4, dokumentacja zawiera jasne wyjaśnienie użycia localStorageżartu. Jednak funkcje pozorowane nie mogły wykonać żadnej z localStoragemetod.

Poniżej znajduje się szczegółowy przykład mojego komponentu reagowania, który wykorzystuje abstrakcyjne metody zapisu i odczytu danych,

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

Błąd:

TypeError: _setupLocalStorage2.default.setItem is not a function

Fix:
Dodaj poniżej funkcji makiety jest (dla ścieżki: .jest/mocks/setUpStore.js)

let mockStorage = {};

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

Odwołanie do fragmentu jest stąd


2

Odsunęłam tutaj kilka innych odpowiedzi, aby rozwiązać problem dla projektu za pomocą Typescript. Utworzyłem LocalStorageMock w następujący sposób:

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]
    }
}

Następnie utworzyłem klasę LocalStorageWrapper, której używam do uzyskiwania dostępu do lokalnej pamięci w aplikacji zamiast bezpośredniego dostępu do globalnej zmiennej lokalnej pamięci. Ułatwiono umieszczenie makiety w opakowaniu do testów.


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

    });
});

Utwórz makietę i dodaj ją do globalobiektu


2

Możesz użyć tego podejścia, aby uniknąć kpiny.

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

2

Musisz mockować pamięć lokalną za pomocą tych fragmentów

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

A w żartobliwej konfiguracji:

"setupFiles":["localStorage.js"]

Nie wahaj się zapytać o wszystko.


1

Następujące rozwiązanie jest kompatybilne do testowania z bardziej rygorystyczną konfiguracją TypeScript, ESLint, TSLint i 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, aby dowiedzieć się, jak zaktualizować global.localStorage


1

Aby zrobić to samo w skrypcie, wykonaj następujące czynności:

Skonfiguruj plik o następującej zawartości:

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

Następnie dodaj następujący wiersz do pliku package.json w ramach konfiguracji Jest

"setupTestFrameworkScriptFile":"PATH_TO_YOUR_FILE",

Lub importujesz ten plik w swoim przypadku testowym, w którym chcesz mockować lokalny magazyn.


0

To zadziałało dla mnie,

delete global.localStorage;
global.localStorage = {
getItem: () => 
 }
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.