126

有没有可以模拟的库localStorage

我一直在使用Sinon.JS进行大多数其他 javascript 模拟,并且发现它真的很棒。

我的初步测试表明 localStorage 拒绝在 firefox 中分配(sadface)所以我可能需要对此进行某种破解:/

我现在的选择(如我所见)如下:

  1. 创建我所有代码都使用的包装函数并模拟它们
  2. 为 localStorage 创建某种(可能很复杂)状态管理(测试前的快照 localStorage,在清理恢复快照中)。
  3. ??????

您如何看待这些方法,您认为还有其他更好的方法可以解决这个问题吗?无论哪种方式,我都会将最终制作的“库”放在 github 上,以实现开源优势。

4

17 回答 17

149

这是用 Jasmine 模拟它的简单方法:

let localStore;

beforeEach(() => {
  localStore = {};

  spyOn(window.localStorage, 'getItem').and.callFake((key) =>
    key in localStore ? localStore[key] : null
  );
  spyOn(window.localStorage, 'setItem').and.callFake(
    (key, value) => (localStore[key] = value + '')
  );
  spyOn(window.localStorage, 'clear').and.callFake(() => (localStore = {}));
});

如果您想在所有测试中模拟本地存储,请在测试beforeEach()的全局范围内声明上面显示的函数(通常的位置是specHelper.js脚本)。

于 2013-01-17T15:04:18.903 回答
57

只需根据您的需要模拟全局 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();
于 2014-10-03T11:07:36.890 回答
23

还要考虑在对象的构造函数中注入依赖项的选项。

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');
于 2013-11-22T20:04:37.507 回答
23

当前的解决方案不适用于 Firefox。这是因为 localStorage 被 html 规范定义为不可修改。但是,您可以通过直接访问 localStorage 的原型来解决这个问题。

跨浏览器解决方案是模拟对象,Storage.prototype例如

而不是spyOn(localStorage, 'setItem')使用

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

取自bzbarsky​​teogeos的回复https://github.com/jasmine/jasmine/issues/299

于 2019-01-04T01:02:19.563 回答
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,
});
于 2016-03-03T10:11:22.110 回答
6

有没有可以模拟的库localStorage

我只写了一个:

(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 中分配

仅在全球范围内。使用上面的包装函数,它工作得很好。

于 2012-07-14T18:34:55.257 回答
5

您不必将存储对象传递给使用它的每个方法。相反,您可以为任何涉及存储适配器的模块使用配置参数。

你的旧模块

// 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)
于 2016-11-07T19:03:16.993 回答
5

如某些答案中所建议的那样覆盖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 });

这种方法的好处是您不必修改要测试的代码。

于 2016-02-10T15:18:18.623 回答
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();
于 2015-03-02T14:12:36.000 回答
4

归功于 https://medium.com/@armno/til-mocking-localstorage-and-sessionstorage-in-angular-unit-tests-a765abdc9d87 制作一个假的本地存储,并在调用本地存储时监视它

 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(...
  });
于 2020-03-13T16:08:13.313 回答
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 一样: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,以便它们不会作为常规项目被迭代、访问或删除,并且不会计入长度。我还添加了一种注册回调的方法,当一个项目即将被放入对象时调用它。此回调可用于模拟测试中超出配额的错误。

于 2015-04-14T19:19:56.273 回答
2

我发现我不需要嘲笑它。我可以通过 将实际的本地存储更改为我想要的状态setItem,然后只需查询值以查看它是否通过getItem. 它不像模拟那么强大,因为你看不到某些东西被改变了多少次,但它对我的目的有用。

于 2019-06-04T17:14:59.823 回答
0

不幸的是,我们可以在测试场景中模拟 localStorage 对象的唯一方法是更改​​我们正在测试的代码。您必须将代码包装在一个匿名函数中(无论如何您都应该这样做)并使用“依赖注入”来传递对窗口对象的引用。就像是:

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

然后,在您的测试中,您可以指定:

window.mockWindow = { localStorage: { ... } };
于 2013-03-06T21:30:10.543 回答
0

需要与存储的数据交互
一个很短的方法

const store = {};
Object.defineProperty(window, 'localStorage', { 
  value: {
    getItem:(key) => store[key]},
    setItem:(key, value) => {
      store[key] = value.toString();
    },
    clear: () => {
      store = {};
    }
  },
});

使用 Jasmine
监视如果您只需要这些函数来使用 jasmine 监视它们,那么它会更短且更易于阅读。

Object.defineProperty(window, 'localStorage', { 
  value: {
    getItem:(key) => {},
    setItem:(key, value) => {},
    clear: () => {},
    ...
  },
});

const spy = spyOn(localStorage, 'getItem')

现在你根本不需要商店。

于 2020-12-07T15:38:09.723 回答
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;
      });

    });
  });
于 2017-12-06T15:33:29.650 回答
-1

我知道 OP 专门询问了关于嘲笑的问题,但可以说它spymock. 如果你用它Object.keys(localStorage)来遍历所有可用的键呢?你可以像这样测试它:

const someFunction = () => {
  const localStorageKeys = Object.keys(localStorage)
  console.log('localStorageKeys', localStorageKeys)
  localStorage.removeItem('whatever')
}

测试代码如下:

describe('someFunction', () => {
  it('should remove some item from the local storage', () => {
    const _localStorage = {
      foo: 'bar', fizz: 'buzz'
    }

    Object.setPrototypeOf(_localStorage, {
      removeItem: jest.fn()
    })

    jest.spyOn(global, 'localStorage', 'get').mockReturnValue(_localStorage)

    someFunction()

    expect(global.localStorage.removeItem).toHaveBeenCalledTimes(1)
    expect(global.localStorage.removeItem).toHaveBeenCalledWith('whatever')
  })
})

不需要模拟或构造函数。行数也相对较少。

于 2021-04-13T09:54:40.810 回答
-1

这些答案都不是完全准确或安全使用的。这也不是,但它和我想要的一样准确,而无需弄清楚如何操作 getter 和 setter。

打字稿

const mockStorage = () => {
  for (const storage of [window.localStorage, window.sessionStorage]) {
    let store = {};

    spyOn(storage, 'getItem').and.callFake((key) =>
      key in store ? store[key] : null
    );
    spyOn(storage, 'setItem').and.callFake(
      (key, value) => (store[key] = value + '')
    );
    spyOn(storage, 'removeItem').and.callFake((key: string) => {
      delete store[key];
    });
    spyOn(storage, 'clear').and.callFake(() => (store = {}));
    spyOn(storage, 'key').and.callFake((i: number) => {
      throw new Error(`Method 'key' not implemented`);
    });
    // Storage.length is not supported
    // Property accessors are not supported
  }
};

用法

describe('Local storage', () => {
  beforeEach(() => {
    mockStorage();
  });

  it('should cache a unit in session', () => {
    LocalStorageService.cacheUnit(testUnit);
    expect(window.sessionStorage.setItem).toHaveBeenCalledTimes(1);
    expect(window.sessionStorage.getItem(StorageKeys.units)).toContain(
      testUnit.id
    );
  });
});

注意事项

  • 使用 localStorage 你可以做到window.localStorage['color'] = 'red';这将绕过模拟。
  • window.localStorage.length将绕过这个模拟。
  • window.localStorage.key抛出这个模拟,因为依赖于此的代码不能被这个模拟测试。
  • 模拟正确地分离了本地和会话存储。

另请参阅:MDN:Web 存储 API

于 2021-11-02T17:02:39.717 回答