JavaScript単体テストでlocalStorageをモックする方法は?


103

モックするライブラリはありますlocalStorageか?

私は他のほとんどのJavaScript モックにSinon.JSを使用してきましたが、それが本当に素晴らしいことを発見しました。

私の最初のテストでは、localStorageがFirefox(悲しい顔)で割り当て可能であることを拒否しているため、おそらくこれを回避するための何らかのハックが必要になります。

今のところ(私の見るとおり)、私のオプションは次のとおりです。

  1. すべてのコードが使用するラッピング関数を作成し、それらをモックします
  2. localStorageのある種の(複雑になる可能性がある)状態管理(テスト前のスナップショットlocalStorage、クリーンアップリストアスナップショット)を作成します。
  3. ??????

あなたはこれらのアプローチについてどう思いますか、そしてあなたはこれについて取り組む他のより良い方法があると思いますか?どちらの方法でも、結果として得られる「ライブラリ」を配置して、最終的にはオープンソースの良さのためにgithubに作成します。


34
#4を逃した:Profit!
Chris Laplante

回答:


128

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

すべてのテストでローカルストレージをモックしたい場合beforeEach()は、テストのグローバルスコープで上記の関数を宣言します(通常、場所はspecHelper.jsスクリプトです)。


1
+1-sinonでもこれを行うことができます。キーはただのメソッドをモック、全体のlocalStorageオブジェクトを模擬するために抱き合わせわざわざ理由です(のgetItemおよび/またはSetItem関数)に興味があります。
s1mm0t


4
私が取得ReferenceError: localStorage is not defined(FB冗談とNPMを使用して、実行中のテスト)...回避する方法任意のアイデア?
FeifanZ 2014年

1
スパイを試すwindow.localStorage
ベンジ2015

21
andCallFakeand.callFakeジャスミン2. +に変更
Venugopal

51

必要に応じて、グローバルlocalStorage / sessionStorage(同じAPIを持つ)をモックするだけです。
例えば:

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

そして、あなたが実際に行うことは、そのようなものです:

// mock the localStorage
window.localStorage = storageMock();
// mock the sessionStorage
window.sessionStorage = storageMock();

1
提案を編集:値が存在しない場合にgetItem返さnullれる必要があります:return storage[key] || null;;
cyberwombat

8
2016年現在、これは最新のブラウザー(ChromeおよびFirefoxをチェック)では機能しないようです。localStorage全体としてオーバーライドすることはできません。
jakub.g

2
ええ、残念ながら、これはもう機能しませんが、それもstorage[key] || null正しくないと主張します。代わりstorage[key] === 0に戻る場合null。あなたならできると思いますreturn key in storage ? storage[key] : null
redbmk 2016

SOでこれを使用しただけです!チャームのように機能しますfunction 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ノードを10.15.1に更新するとエラーが発生しますTypeError: Cannot set property localStorage of #<Window> which has only a getter。これを修正するにはどうすればよいですか?
Tasawer Nawaz

19

また、オブジェクトのコンストラクター関数に依存関係を注入するオプションも検討してください。

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)

モッキングと単体テストに沿って、私はストレージ実装のテストを避けたいと思います。たとえば、アイテムを設定した後にストレージの長さが増加したかどうかを確認しても意味がありません。

実際のlocalStorageオブジェクトのメソッドを置き換えることは明らかに信頼性が低いため、「ダム」のmockStorageを使用し、以下のように、必要に応じて個々のメソッドをスタブします。

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

1
この質問を見たのは久しぶりだと思いますが、これが実際に私がやったことです。
Anthony Sottile 2013年

1
これは、時間を破るリスクがそれほど高くないため、価値のある唯一のソリューションです。
オリゴフレン2016

14

これが私がすることです...

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

12

現在のソリューションはFirefoxでは機能しません。これは、localStorageがhtml仕様で変更不可として定義されているためです。ただし、localStorageのプロトタイプに直接アクセスすることで、これを回避できます。

クロスブラウザソリューションは、Storage.prototypeたとえば、オブジェクトをモックすることです

spyOn(localStorage、 'setItem')の代わりに使用

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

bzbarskyteogeosの返信からここに取られましhttps://github.com/jasmine/jasmine/issues/299


1
あなたのコメントはもっといいね!ありがとうございました!
LorisBachert

6

モックするライブラリはありますlocalStorageか?

私は1つ書いた:

(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

})();

私の最初のテストは、localStorageがFirefoxで割り当て可能であることを拒否していることを示しています

グローバルコンテキストでのみ。上記のラッパー関数を使用すると、問題なく動作します。


1
次も使用できますvar window = { localStorage: ... }
user123444555621

1
残念ながら、これは私が必要とし、ウィンドウオブジェクトに追加したすべてのプロパティを知る必要があることを意味します(そして、そのプロトタイプなどを見逃しています)。jQueryが必要とするものは何でも含めます。残念ながら、これは非解決策のようです。また、テストはを使用するコードをlocalStorageテストしていlocalStorageます。テストは必ずしもその中に直接あるとは限りません。このソリューションはlocalStorage他のスクリプトのを変更しないため、ソリューションではありません。スコーピングトリックの+1
Anthony Sottile

1
テスト可能にするには、コードを調整する必要がある場合があります。私はこれが非常に煩わしいことを知っています、そしてそれが私が単体テストより重いセレンテストを好む理由です。
user123444555621

これは有効なソリューションではありません。その無名関数内から関数を呼び出すと、モックウィンドウまたはモックlocalStorageオブジェクトへの参照が失われます。単体テストの目的は、外部関数を呼び出すことです。そのため、localStorageで機能する関数を呼び出す場合、モックは使用されません。代わりに、テストするコードを無名関数でラップする必要があります。テスト可能にするには、ウィンドウオブジェクトをパラメーターとして受け入れます。
John Kurlak 2013年

そのモックにはバグがあります。存在しないアイテムを取得すると、getItemはnullを返します。モックでは、未定義を返します。正しいコードは次のようになりますif this.hasOwnProperty(key) return this[key] else return null
Evan

4

以下は、sinon spyと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();

4

一部の回答で提案されているようにlocalStorageグローバルwindowオブジェクトのプロパティを上書きしても、ほとんどのJSエンジンではlocalStorage機能しません。これは、データプロパティが書き込み可能ではなく、構成不可能であると宣言されているためです。

ただし、少なくともPhantomJS(バージョン1.9.8)のWebKitバージョンでは、レガシーAPI __defineGetter__を使用して、localStorageアクセスされた場合の動作を制御できることがわかりました。それでも、これが他のブラウザでも機能する場合は興味深いでしょう。

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

このアプローチの利点は、テストするコードを変更する必要がないことです。


これはPhantomJS 2.1.1では機能しないことに注意してください。;)
コンラッドカルメス2016

4

ストレージオブジェクトを使用する各メソッドに渡す必要はありません。代わりに、ストレージアダプターに接続するすべてのモジュールの構成パラメーターを使用できます。

古いモジュール

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

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

「ラッパー」機能を設定した新しいモジュール

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

モジュールをテストコードで使用する場合

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

MockStorageクラスは次のようになります。

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

モジュールを本番コードで使用する場合は、実際のlocalStorageアダプターを渡します

const myModule = require('./my-module')(window.localStorage)

これはes6でのみ有効です:developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…(ただし、優れたソリューションであり、どこでも利用可能になるまで待つことができません!)
Alex Moore-ニエミ

@ AlexMoore-NiemiここではES6の使用はほとんどありません。変更はほとんどなく、ES5以下を使用してすべて実行できます。
ありがとう

うん、ちょうどexport default functionそのような引数を持つモジュールを指摘して初期化するのはes6だけです。パターンは関係ありません。
Alex Moore-Niemi 2016

えっ?古いスタイルを使用しrequireてモジュールをインポートし、同じ式の引数に適用する必要がありました。私が知っているES6でそれを行う方法はありません。それ以外の場合は、ES6を使用していimport
ました

2

私は、Pumbaa80の回答に対するコメントを別の回答として繰り返して、ライブラリとして再利用しやすくすることにしました。

私はPumbaa80のコードを取得し、少し改良して、テストを追加し、npmモジュールとしてここに公開しました:https ://www.npmjs.com/package/mock-local-storage 。

これがソースコードです:https : //github.com/letsrock-today/mock-local-storage/blob/master/src/mock-localstorage.js

一部のテスト:https : //github.com/letsrock-today/mock-local-storage/blob/master/test/mock-localstorage.js

モジュールは、グローバルオブジェクト(ウィンドウまたはグローバル、どちらが定義されているか)にモックlocalStorageおよびsessionStorageを作成します。

他のプロジェクトのテストmocha -r mock-local-storageでは、テスト対象のすべてのコードでグローバル定義を使用できるようにするために、モカを次のように必要としました。

基本的に、コードは次のようになります。

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

経由で追加されたすべてのメソッドはObject.defineProperty、通常のアイテムとして繰り返されたり、アクセスされたり、削除されたり、長さを数えたりしないようになっています。また、アイテムがオブジェクトに配置されようとしているときに呼び出されるコールバックを登録する方法を追加しました。このコールバックは、テストで割り当て超過エラーをエミュレートするために使用できます。


2

私はそれをあざける必要がないことに気づきました。実際のローカルストレージを必要な状態にsetItem変更してから、値をクエリして、によって変更されたかどうかを確認できgetItemます。それは、何かが変更された回数を確認できないほどモックほど強力ではありませんが、私の目的には役立ちました。


0

残念ながら、テストシナリオでlocalStorageオブジェクトをモックできる唯一の方法は、テストするコードを変更することです。匿名関数(とにかく行う必要があります)でコードをラップし、「依存性注入」を使用してウィンドウオブジェクトへの参照を渡す必要があります。何かのようなもの:

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

次に、テスト内で、以下を指定できます。

window.mockWindow = { localStorage: { ... } };

0

これが私のやり方です。シンプルに保つ。

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

    });
  });

0

クレジット https://medium.com/@armno/til-mocking-localstorage-and-sessionstorage-in-angular-unit-tests-a765abdc9d87は それがcaleldあるとき、偽ののlocalStorageを作成し、localStorageををスパイ

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

そして、ここでそれを使用します

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

    expect(...
  });
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.