如何在JavaScript单元测试中模拟localStorage?


103

有没有可以模拟的库localStorage

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

我的初步测试表明,localStorage拒绝在firefox(sadface)中分配,因此我可能需要对此进行一些修改:/

到目前为止,我的选择如下所示:

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

您如何看待这些方法,您是否认为还有其他更好的方法可以做到这一点?无论哪种方式,我都会将最终生成的“库”放到github上以获取开放源代码。


34
您错过了#4:Profit!
Chris Laplante 2012年

Answers:


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

6
抬头:Firefox中的此解决方案似乎存在问题:github.com/pivotal/jasmine/issues/299
cthulhu 2013年

4
我得到了ReferenceError: localStorage is not defined(使用FB Jest和npm运行测试)...有什么解决方法?
FeifanZ

1
尝试监视window.localStorage
Benj 2015年

21
andCallFake改为and.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

刚刚在SO上使用过!就像魅力一样工作-在真实服务器上时,只需将localStor更改回localStoragefunction 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
这是唯一有价值的解决方案,因为它没有那么高的时间中断风险。
oligofren

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中使用。这是因为html规范将localStorage定义为不可修改的。但是,您可以通过直接访问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

我只写了一个:

(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 2012年

1
不幸的是,这意味着我将需要知道我需要的每个属性,并将其添加到window对象中(而我错过了它的原型,等等)。包括jQuery可能需要的任何内容。不幸的是,这似乎不是解决方案。哦,测试是使用的测试代码,localStorage测试不一定localStorage直接包含在其中。此解决方案不会更改localStorage其他脚本的,因此不是解决方案。范围设定技巧为+1
Anthony Sottile 2012年

1
您可能需要调整代码以使其可测试。我知道这很烦人,这就是为什么我更喜欢重硒测试而不是单元测试的原因。
user123444555621 2012年

这不是有效的解决方案。如果从该匿名函数中调用任何函数,则将丢失对模拟窗口或模拟localStorage对象的引用。单元测试的目的是您必须调用外部函数。因此,当您调用与localStorage一起使用的函数时,它将不会使用该模拟。相反,您必须将要测试的代码包装在匿名函数中。为了使其可测试,请使其接受window对象作为参数。
John Kurlak 2013年

该模拟存在一个错误:检索不存在的项目时,getItem应该返回null。在模拟中,它返回未定义。正确的代码应为if this.hasOwnProperty(key) return this[key] else return null
Evan,

4

这是一个使用sinon间谍和模拟的例子:

// 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引擎中将不起作用,因为它们将localStoragedata属性声明为不可写且不可配置。

但是我发现至少使用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中不起作用。;)
康拉德·卡尔梅兹

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/zh-CN/docs/Web/JavaScript/Reference/…(但这是一个很好的解决方案,我迫不及待要等到任何地方都可以使用!)
Alex Moore- Niemi

@ AlexMoore-Niemi这里很少使用ES6。所有这些都可以使用ES5或更低版本进行,只需很少的更改即可完成。
谢谢您

是的,只是指出export default function并使用像es6这样的arg初始化模块。该模式无论如何都保持不变。
Alex Moore-Niemi's

??我不得不使用较旧的样式require来导入模块,并将其应用于同一表达式中的参数。据我所知,在ES6中无法做到这一点。否则,我会用过ES6import
谢谢您

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都不会作为常规项进行迭代,访问或删除,也不会计入长度。另外,我还添加了一种注册回调的方法,该回调将在将要放入对象的项目中调用。此回调可用于模拟测试中超出配额的错误。


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 做一个假的localStorage和窥视本地存储,当它是caleld

 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(...
  });
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.