JavaScript 단위 테스트에서 localStorage를 모의하는 방법은 무엇입니까?


103

조롱 할 라이브러리가 있습니까? localStorage 있습니까?

나는 대부분의 다른 자바 스크립트 조롱에 Sinon.JS 를 사용해 왔으며 정말 훌륭하다는 것을 알았습니다.

내 초기 테스트는 localStorage가 firefox (sadface)에서 할당을 거부한다는 것을 보여 주므로 아마도 이것에 대해 일종의 해킹이 필요할 것입니다.

현재 내 옵션은 다음과 같습니다.

  1. 내 모든 코드에서 사용하는 래핑 함수를 만들고 모의
  2. localStorage에 대한 일종의 (복잡 할 수 있음) 상태 관리 (테스트 전 스냅 샷 localStorage, 정리 복원 스냅 샷에서)를 만듭니다.
  3. ??????

이러한 접근 방식에 대해 어떻게 생각하고 이에 대해 더 나은 방법이 있다고 생각하십니까? 어느 쪽이든 저는 오픈 소스의 장점을 위해 github에 최종적으로 만드는 "라이브러리"를 넣을 것입니다.


34
: 당신은 # 4 놓친Profit!
크리스 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)를 모의하기
만하면

6
헤드 업 : 파이어 폭스에서이 솔루션에 문제가있는 것 같습니다 : github.com/pivotal/jasmine/issues/299
크 툴루

4
내가 얻을 ReferenceError: localStorage is not defined방법 ... 어떤 아이디어 (FB 농담과 NPM을 사용하여 테스트를 실행하는) 주변의 일을?
FeifanZ

1
감시 시도window.localStorage
Benj

21
andCallFakeand.callFakejasmine 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 2015

8
2016 년 현재 최신 브라우저 (Chrome 및 Firefox 확인)에서는 작동하지 않는 것 같습니다. localStorage전체적으로 재정의 하는 것은 불가능합니다.
jakub.g

2
예, 안타깝게도이 기능은 더 이상 작동하지 않지만 storage[key] || null잘못된 것 같습니다. 경우 storage[key] === 0는 반환됩니다 null대신. 그래도 할 수 있다고 생각합니다 return key in storage ? storage[key] : null.
redbmk

그냥 이렇게 사용했습니다! 마법처럼 작품 - 다만 실제 서버에 로컬 스토리지에 localStor을 다시 변경해야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 개체에서 메서드를 교체하는 것은 분명히 신뢰할 수 없기 때문에 "dumb"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
나는이 질문을 본지 오래되었다는 것을 알고 있습니다. 그러나 이것은 실제로 제가 한 일입니다.
안토니 Sottile

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에서 작동하지 않습니다. 이는 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 있습니까?

방금 썼습니다.

(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
안토니 Sottile

1
코드를 테스트 할 수 있도록 수정해야 할 수도 있습니다. 나는 이것이 매우 성가시다는 것을 알고 있으며, 이것이 내가 단위 테스트보다 무거운 셀레늄 테스트를 선호하는 이유입니다.
user123444555621 jul.

이것은 유효한 해결책이 아닙니다. 해당 익명 함수 내에서 함수를 호출하면 모의 창 또는 모의 localStorage 개체에 대한 참조를 잃게됩니다. 단위 테스트의 목적은 외부 함수를 호출하는 것입니다. 따라서 localStorage와 함께 작동하는 함수를 호출하면 mock을 사용하지 않습니다. 대신 테스트중인 코드를 익명 함수로 래핑해야합니다. 테스트 가능하게하려면 창 개체를 매개 변수로 받아들이도록합니다.
John Kurlak 2013 년

그 모의에는 버그가 있습니다. 존재하지 않는 항목을 검색 할 때 getItem은 null을 반환해야합니다. 모의에서는 undefined를 반환합니다. 올바른 코드는 다음과 같아야합니다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에서는이 기능이 작동하지 않습니다. ;)
Conrad Calmez

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- Niemi

@ AlexMoore-Niemi 여기서 ES6를 거의 사용하지 않습니다. 거의 변경없이 ES5 이하를 사용하여 모든 작업을 수행 할 수 있습니다.
감사합니다

네, 그냥 export default functiones6과 같은 arg로 모듈을 지적 하고 초기화합니다. 패턴은 무관합니다.
알렉스 무어 - 니에미

어? 이전 스타일을 사용하여 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중인 모든 코드에 대해 전역 정의를 사용할 수 있도록하기 위해 mocha와 함께 필요했습니다 .

기본적으로 코드는 다음과 같습니다.

(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를 감시하십시오.

 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.