Bagaimana cara mengejek localStorage dalam pengujian unit JavaScript?

103

Apakah ada perpustakaan di luar sana untuk diolok-olok localStorage?

Saya telah menggunakan Sinon.JS untuk sebagian besar ejekan javascript saya yang lain dan merasa sangat hebat.

Pengujian awal saya menunjukkan bahwa localStorage menolak untuk ditugaskan di firefox (sadface) jadi saya mungkin perlu semacam peretasan untuk ini: /

Pilihan saya saat ini (seperti yang saya lihat) adalah sebagai berikut:

  1. Buat fungsi pembungkus yang digunakan semua kode saya dan tiru itu
  2. Buat semacam manajemen status (mungkin rumit) (snapshot localStorage sebelum pengujian, di snapshot pemulihan pembersihan) untuk localStorage.
  3. ??????

Apa pendapat Anda tentang pendekatan ini dan apakah menurut Anda ada cara lain yang lebih baik untuk melakukannya? Either way saya akan menempatkan "perpustakaan" yang dihasilkan yang akhirnya saya buat di github untuk kebaikan open source.

Anthony Sottile
sumber
34
Anda melewatkan # 4:Profit!
Chris Laplante

Jawaban:

128

Berikut cara sederhana untuk mengejeknya dengan 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 = {};
  });
});

Jika Anda ingin meniru penyimpanan lokal dalam semua pengujian Anda, nyatakan beforeEach()fungsi yang ditampilkan di atas dalam cakupan global pengujian Anda (tempat biasanya adalah skrip specHelper.js ).

Andreas Köberle
sumber
1
+1 - Anda juga dapat melakukan ini dengan sinon. Kuncinya adalah mengapa repot-repot mengikat untuk memalsukan seluruh objek localStorage, cukup tiru metode (getItem dan / atau setItem) yang Anda minati.
s1mm0t
6
Perhatian: Sepertinya ada masalah dengan solusi ini di Firefox: github.com/pivotal/jasmine/issues/299
cthulhu
4
Saya mendapatkan ReferenceError: localStorage is not defined(menjalankan tes menggunakan FB Jest dan npm)… ada ide bagaimana untuk menyiasatinya?
FeifanZ
1
Cobalah memata-mataiwindow.localStorage
Benj
22
andCallFakediubah menjadi and.callFakedi Jasmine 2. +
Venugopal
51

cukup mengejek localStorage / sessionStorage global (mereka memiliki API yang sama) untuk kebutuhan Anda.
Sebagai contoh:

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

Dan kemudian apa yang sebenarnya Anda lakukan, adalah seperti itu:

// mock the localStorage
window.localStorage = storageMock();
// mock the sessionStorage
window.sessionStorage = storageMock();
a8m
sumber
1
Edit saran: getItemharus dikembalikan nulljika nilai tidak ada return storage[key] || null;:;
cyberwombat
8
Pada 2016, Tampaknya ini tidak berfungsi di browser modern (periksa Chrome dan Firefox); mengesampingkan localStoragesecara keseluruhan tidak mungkin.
jakub.g
2
Ya, sayangnya ini tidak berfungsi lagi, tetapi saya juga berpendapat itu storage[key] || nulltidak benar. Jika storage[key] === 0akan kembali nullsebagai gantinya. Saya pikir Anda bisa melakukannya return key in storage ? storage[key] : null.
redbmk
Baru saja menggunakan ini di SO! Bekerja seperti pesona - hanya perlu mengubah localStor kembali ke localStorage ketika di server nyatafunction 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 Saya mendapatkan error setelah node update ke 10.15.1 itu TypeError: Cannot set property localStorage of #<Window> which has only a getter, tahu bagaimana cara memperbaikinya?
Tasawer Nawaz
19

Pertimbangkan juga opsi untuk memasukkan dependensi dalam fungsi konstruktor objek.

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)

Sejalan dengan mocking dan pengujian unit, saya ingin menghindari pengujian implementasi penyimpanan. Misalnya tidak ada gunanya memeriksa apakah panjang penyimpanan bertambah setelah Anda mengatur item, dll.

Karena jelas tidak dapat diandalkan untuk mengganti metode pada objek localStorage yang sebenarnya, gunakan mockStorage "dumb" dan hentikan metode individual seperti yang diinginkan, seperti:

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
sumber
1
Saya menyadari sudah lama sejak saya tidak melihat pertanyaan ini - tetapi sebenarnya inilah yang akhirnya saya lakukan.
Anthony Sottile
1
Ini adalah satu-satunya solusi yang bermanfaat, karena tidak memiliki risiko tinggi untuk merusak waktu.
oligofren
14

Inilah yang saya lakukan...

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

Solusi saat ini tidak akan berfungsi di Firefox. Ini karena localStorage didefinisikan oleh spesifikasi html sebagai tidak dapat dimodifikasi. Namun Anda bisa menyiasatinya dengan mengakses prototipe localStorage secara langsung.

Solusi lintas browser adalah mengejek objek pada Storage.prototypemis

bukannya spyOn (localStorage, 'setItem') digunakan

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

diambil dari balasan bzbarsky dan teogeos di sini https://github.com/jasmine/jasmine/issues/299

roo2
sumber
1
Komentar Anda harus mendapatkan lebih banyak suka. Terima kasih!
LorisBachert
6

Apakah ada perpustakaan di luar sana untuk diolok-olok localStorage?

Saya baru saja menulis satu:

(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

})();

Pengujian awal saya menunjukkan bahwa localStorage menolak untuk ditetapkan di firefox

Hanya dalam konteks global. Dengan fungsi pembungkus seperti di atas, ini berfungsi dengan baik.

pengguna123444555621
sumber
1
Anda juga dapat menggunakanvar window = { localStorage: ... }
user123444555621
1
Sayangnya itu berarti saya perlu mengetahui setiap properti yang saya perlukan dan telah ditambahkan ke objek jendela (dan saya kehilangan prototipe, dll.). Termasuk apa pun yang mungkin dibutuhkan jQuery. Sayangnya ini sepertinya bukan solusi. Oh juga, tes adalah kode pengujian yang digunakan localStorage, tes tidak harus localStoragelangsung di dalamnya. Solusi ini tidak mengubah localStorageuntuk skrip lain jadi ini bukan solusi. +1 untuk trik pelingkupan
Anthony Sottile
1
Anda mungkin perlu menyesuaikan kode Anda agar dapat diuji. Saya tahu ini sangat menjengkelkan, dan itulah mengapa saya lebih suka pengujian selenium yang berat daripada pengujian unit.
pengguna123444555621
Ini bukan solusi yang valid. Jika Anda memanggil fungsi apa pun dari dalam fungsi anonim tersebut, Anda akan kehilangan referensi ke jendela tiruan atau objek localStorage tiruan. Tujuan dari pengujian unit adalah agar Anda DO memanggil fungsi luar. Jadi, saat Anda memanggil fungsi Anda yang bekerja dengan localStorage, itu tidak akan menggunakan tiruan. Sebaliknya, Anda harus membungkus kode yang Anda uji dalam fungsi anonim. Untuk membuatnya dapat diuji, minta itu menerima objek jendela sebagai parameter.
John Kurlak
Tiruan itu memiliki bug: Saat mengambil item yang tidak ada, getItem harus mengembalikan null. Dalam tiruan, itu kembali tidak ditentukan. Kode yang benar harusif this.hasOwnProperty(key) return this[key] else return null
Evan
4

Berikut adalah contoh penggunaan sinon spy dan 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
sumber
4

Menimpa localStorageproperti windowobjek global seperti yang disarankan dalam beberapa jawaban tidak akan berfungsi di sebagian besar mesin JS, karena mereka mendeklarasikan localStorageproperti data sebagai tidak dapat ditulis dan tidak dapat dikonfigurasi.

Namun saya menemukan bahwa setidaknya dengan versi WebKit PhantomJS (versi 1.9.8) Anda dapat menggunakan API lama __defineGetter__untuk mengontrol apa yang terjadi jika localStoragediakses. Tetap akan menarik jika ini berfungsi di browser lain juga.

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

Manfaat dari pendekatan ini adalah Anda tidak perlu mengubah kode yang akan Anda uji.

Conrad Calmez
sumber
Hanya perhatikan bahwa ini tidak akan berfungsi di PhantomJS 2.1.1. ;)
Conrad Calmez
4

Anda tidak harus meneruskan objek penyimpanan ke setiap metode yang menggunakannya. Sebagai gantinya, Anda dapat menggunakan parameter konfigurasi untuk modul apa pun yang menyentuh adaptor penyimpanan.

Modul lama Anda

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

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

Modul baru Anda dengan fungsi config "wrapper"

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

Saat Anda menggunakan modul dalam menguji kode

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

The MockStoragekelas akan terlihat seperti ini

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

Saat menggunakan modul Anda dalam kode produksi, teruskan adaptor localStorage yang sebenarnya

const myModule = require('./my-module')(window.localStorage)
Terima kasih
sumber
Untuk teman-teman, ini hanya berlaku di es6: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… (tetapi ini adalah solusi yang bagus dan saya tidak bisa menunggu sampai tersedia di mana-mana!)
Alex Moore- Niemi
@ AlexMoore-Niemi penggunaan ES6 sangat sedikit di sini. Semua itu bisa dilakukan menggunakan ES5 atau lebih rendah dengan sedikit perubahan.
Terima kasih
ya, cukup tunjukkan export default functiondan inisialisasi modul dengan argumen seperti itu hanya es6. polanya tetap berdiri sendiri.
Alex Moore-Niemi
Hah? Saya harus menggunakan gaya yang lebih lama requireuntuk mengimpor modul dan menerapkannya ke argumen dalam ekspresi yang sama. Tidak ada cara untuk melakukan itu di ES6 yang saya tahu. Kalau tidak, saya akan menggunakan ES6import
Terima kasih
2

Saya memutuskan untuk mengulangi komentar saya atas jawaban Pumbaa80 sebagai jawaban terpisah agar lebih mudah untuk digunakan kembali sebagai perpustakaan.

Saya mengambil kode Pumbaa80, memperbaikinya sedikit, menambahkan tes dan menerbitkannya sebagai modul npm di sini: https://www.npmjs.com/package/mock-local-storage .

Berikut ini kode sumbernya: https://github.com/letsrock-today/mock-local-storage/blob/master/src/mock-localstorage.js

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

Modul membuat localStorage dan sessionStorage tiruan pada objek global (jendela atau global, yang mana yang ditentukan).

Dalam pengujian proyek saya yang lain, saya memerlukannya dengan mocha seperti ini: mocha -r mock-local-storageuntuk membuat definisi global tersedia untuk semua kode yang diuji.

Pada dasarnya, kode terlihat seperti berikut:

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

Perhatikan bahwa semua metode ditambahkan melalui Object.definePropertysehingga tidak akan diulang, diakses atau dihapus sebagai item biasa dan tidak akan dihitung panjangnya. Juga saya menambahkan cara untuk mendaftarkan callback yang dipanggil ketika sebuah item akan dimasukkan ke dalam objek. Callback ini dapat digunakan untuk meniru kesalahan kuota terlampaui dalam pengujian.

nikolay_turpitko
sumber
2

Saya menemukan bahwa saya tidak perlu mengejeknya. Saya dapat mengubah penyimpanan lokal sebenarnya ke keadaan yang saya inginkan setItem, lalu menanyakan nilai untuk melihat apakah itu berubah melalui getItem. Ini tidak sekuat mengejek karena Anda tidak dapat melihat berapa kali sesuatu diubah, tetapi berhasil untuk tujuan saya.

RandomEngy
sumber
0

Sayangnya, satu-satunya cara kita dapat memalsukan objek localStorage dalam skenario pengujian adalah dengan mengubah kode yang kita uji. Anda harus membungkus kode Anda dalam fungsi anonim (yang seharusnya Anda lakukan) dan menggunakan "injeksi ketergantungan" untuk meneruskan referensi ke objek jendela. Sesuatu seperti:

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

Kemudian, di dalam pengujian Anda, Anda dapat menentukan:

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

Ini adalah cara saya melakukannya. Buat tetap sederhana.

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

kredit ke https://medium.com/@armno/til-mocking-localstorage-and-sessionstorage-in-angular-unit-tests-a765abdc9d87 Membuat penyimpanan lokal palsu, dan memata-matai penyimpanan lokal, saat disimpan

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

Dan di sini kami menggunakannya

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

    expect(...
  });
Johansrk
sumber