【问题标题】:How to mock localStorage in JavaScript unit tests?如何在 JavaScript 单元测试中模拟 localStorage?
【发布时间】:2012-07-14 04:27:30
【问题描述】:

有没有可以模拟 localStorage 的库?

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

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

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

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

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

【问题讨论】:

  • 你错过了#4:Profit!

标签: javascript unit-testing mocking local-storage sinon


【解决方案1】:

有没有可以模拟 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 中分配

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

【讨论】:

  • 你也可以使用var window = { localStorage: ... }
  • 不幸的是,这意味着我需要知道我需要并添加到窗口对象的每个属性(我错过了它的原型等)。包括任何 jQuery 可能需要的东西。不幸的是,这似乎不是一个解决方案。哦,还有,测试是使用localStorage 的测试代码,测试中不一定直接有localStorage。此解决方案不会更改其他脚本的 localStorage,因此它不是解决方案。 +1 的范围技巧虽然
  • 您可能需要调整您的代码以使其可测试。我知道这很烦人,这就是为什么我更喜欢繁重的硒测试而不是单元测试。
  • 这不是一个有效的解决方案。如果您从该匿名函数中调用任何函数,您将丢失对模拟窗口或模拟 localStorage 对象的引用。单元测试的目的是调用外部函数。因此,当您调用与 localStorage 一起使用的函数时,它不会使用模拟。相反,您必须将您正在测试的代码包装在一个匿名函数中。为了使其可测试,让它接受窗口对象作为参数。
  • 那个 mock 有一个 bug:当检索一个不存在的项目时,getItem 应该返回 null。在模拟中,它返回未定义。正确的代码应该是if this.hasOwnProperty(key) return this[key] else return null
【解决方案2】:

这是用 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 脚本)。

【讨论】:

  • +1 - 你也可以用 sinon 做到这一点。关键是为什么要费心去模拟整个 localStorage 对象,只模拟你感兴趣的方法(getItem 和/或 setItem)。
  • 注意:Firefox 中的此解决方案似乎存在问题:github.com/pivotal/jasmine/issues/299
  • 我收到了 ReferenceError: localStorage is not defined(使用 FB Jest 和 npm 运行测试)……有什么解决方法的想法吗?
  • 尝试监视window.localStorage
  • andCallFake 在 jasmine 2.+ 中更改为 and.callFake
【解决方案3】:

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

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

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

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

【讨论】:

    【解决方案4】:

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

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

    【讨论】:

    • 我意识到我已经有一段时间没有看到这个问题了——但这实际上是我最终要做的。
    • 这是唯一值得的解决方案,因为它没有那么高的时间中断风险。
    【解决方案5】:

    只需根据您的需要模拟全局 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();
    

    【讨论】:

    • 截至 2016 年,这似乎不适用于现代浏览器(检查 Chrome 和 Firefox);无法整体覆盖localStorage
    • 是的,不幸的是这不再起作用,但我也认为storage[key] || null 是不正确的。如果storage[key] === 0 它将返回null。我认为你可以做return key in storage ? storage[key] : null
    • 刚刚用过这个!像魅力一样工作 - 只需在真实服务器上将 localStor 更改回 localStorage 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();
    • @a8m 将节点更新到 10.15.1 后出现错误,TypeError: Cannot set property localStorage of #<Window> which has only a getter,知道如何解决这个问题吗?
    • 关于setItem,它应该是storage[key] = ${value}`` 而不是storage[key] = value || '',因为你可以做sessionStorage.setItem('foo', undefined),它会将未定义(或null)保存为字符串。
    【解决方案6】:

    这里是一个使用 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();
    

    【讨论】:

      【解决方案7】:

      我决定重申我对 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

      Module 在全局对象上创建模拟 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 添加的方法,这样它们就不会作为常规项目被迭代、访问或删除,并且不会计入长度。我还添加了一种注册回调的方法,当一个项目即将被放入对象时调用该回调。此回调可用于模拟测试中的配额超出错误。

      【讨论】:

        【解决方案8】:

        按照某些答案中的建议覆盖全局window 对象的localStorage 属性在大多数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 中不起作用。 ;)
        【解决方案9】:

        这就是我的工作......

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

        【讨论】:

          【解决方案10】:

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

          你的旧模块

          // 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/…(但这是一个很好的解决方案,我不能等到它无处不在!)
          • @AlexMoore-Niemi 这里很少使用 ES6。所有这些都可以使用 ES5 或更低版本完成,只需很少的更改。
          • 是的,只是指出export default function 并使用类似 es6 的 arg 初始化模块。无论如何,模式都站得住脚。
          • 嗯?我必须使用旧样式 require 来导入模块并将其应用于同一表达式中的参数。据我所知,在 ES6 中没有办法做到这一点。否则我会使用 ES6 import
          【解决方案11】:

          这就是我喜欢的方式。保持简单。

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

          【讨论】:

            【解决方案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

            【讨论】:

            • 你的评论应该会得到更多的赞。谢谢!
            • 同意,这也是解决 Prebid PR 问题的最佳答案!
            【解决方案13】:

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

            【讨论】:

              【解决方案14】:

              归功于 https://medium.com/@armno/til-mocking-localstorage-and-sessionstorage-in-angular-unit-tests-a765abdc9d87 制作一个假的 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(...
                });
              

              【讨论】:

                【解决方案15】:

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

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

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

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

                现在您根本不需要商店。

                【讨论】:

                  【解决方案16】:

                  我知道 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')
                    })
                  })
                  

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

                  【讨论】:

                    【解决方案17】:

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

                    TypeScript

                    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 在这个 mock 中抛出,因为依赖于它的代码不能被这个 mock 测试。
                    • Mock 正确地分离了本地和会话存储。

                    另请参阅:MDN: Web Storage API

                    【讨论】:

                      猜你喜欢
                      • 2015-12-06
                      • 1970-01-01
                      • 2017-06-14
                      • 1970-01-01
                      • 1970-01-01
                      • 2013-10-11
                      • 1970-01-01
                      • 1970-01-01
                      • 2018-12-28
                      相关资源
                      最近更新 更多