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

时间:2012-07-14 16:24:46

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

是否有任何库可以模拟localStorage

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

我的初步测试显示localStorage拒绝在firefox(sadface)中分配,所以我可能需要某种黑客攻击:/

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

  1. 创建我的所有代码使用的包装函数并模拟那些
  2. 为localStorage创建某种(可能很复杂的)状态管理(测试前的快照localStorage,清理恢复快照)。
  3. ??????
  4. 您如何看待这些方法,您认为还有其他更好的方法吗?无论哪种方式,我都会将最终制作的“库”放在github上,以获得开源优势。

16 个答案:

答案 0 :(得分:112)

这是使用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 :(得分:44)

根据您的需要模拟全局localStorage / sessionStorage(它们具有相同的API) 例如:

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

然后你真正做的是这样的事情:

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

答案 2 :(得分: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');

答案 3 :(得分:11)

这就是我做的......

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

答案 4 :(得分: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中分配

仅在全球范围内。使用上面的包装函数,它可以正常工作。

答案 5 :(得分: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();

答案 6 :(得分: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 });

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

答案 7 :(得分:3)

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

您的旧模块

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

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

你的新模块带有config" wrapper"功能

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)

答案 8 :(得分:3)

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

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

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

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

摘自 bzbarsky teogeos 在这里https://github.com/jasmine/jasmine/issues/299

答案 9 :(得分:2)

学分 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(...
  });

答案 10 :(得分:1)

我决定重申我对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添加的,以便它们不会作为常规项目进行迭代,访问或删除,并且不会计算长度。我还添加了一种注册回调的方法,当一个项目即将被放入对象时调用该回调。此回调可用于模拟测试中超出配额的错误。

答案 11 :(得分:0)

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

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

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

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

答案 12 :(得分:0)

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

git remote add gitlab <gitlab_repo_url>
git push --all gitlab

答案 13 :(得分:0)

我发现不需要模拟它。我可以通过setItem将实际的本地存储更改为所需的状态,然后仅查询值以查看是否通过getItem进行了更改。它不像模拟那样强大,因为您看不到更改了多少次,但是它对我有用。

答案 14 :(得分:0)

需要与存储的数据进行交互
一种很短的方法

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

用茉莉花间谍
如果您只需要这些功能即可使用茉莉花对其进行监视,它会更短并且更易于阅读。

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

const spy = spyOn(localStorage, 'getItem')

现在您根本不需要商店。

答案 15 :(得分:-1)

我知道 OP 专门询问了模拟,但可以说最好是 spy 而不是 mock。如果您使用 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')
  })
})

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