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.

Chiedo
źródło

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;
nickcan
źródło
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",

Chiedo
źródło
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

c4k
źródło
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();
Bastian Stein
źródło
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
Dmitriy
źródło
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');
TigerBear
źródło
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.

Carlos Huamani
źródło
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');
Christian Saiki
źródło
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

Mad-D
źródło
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.

CorayThan
źródło
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

Trevor Joseph
źródło
2

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

Storage.prototype.getItem = jest.fn(() => expectedPayload);
Sanath
źródło
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.

Slim Coder
źródło
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

Beau Smith
źródło
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.

vs_lala
źródło
0

To zadziałało dla mnie,

delete global.localStorage;
global.localStorage = {
getItem: () => 
 }
praktyczny
źródło